food-market/tests/food-market.UnitTests/CatalogDtosSmokeTests.cs
nns 9588d03bf4 test(s15): axe a11y + focus traps + unit coverage 80% + property tests + backup drill
Sprint 15 финальный — реальные axe + coverage + pg_restore numbers.

Ключевые цифры:
- axe-core: critical=0 on 10 страниц stage'а; serious 12→9
  после фиксов (sidebar contrast + 8 icon-only back-arrow aria-labels).
- Unit coverage: Application 56%→83%, Domain 11%→79%, combined
  60%→80%. Тестов 68→147 (+79).
- Backup recovery drill: RTO ~25 секунд end-to-end
  (pg_dump 2s + pg_restore 4s + dotnet startup 19s).

Что сделано:
1. @axe-core/playwright + stage-ui-15 (10 страниц) + stage-ui-16
   (SR smoke на login: getByLabel, role=alert, aria-describedby,
   keyboard nav).
2. useFocusTrap hook (WCAG 2.4.3 + 2.1.2): return-focus, mount-focus,
   Tab cycle. Подключён к Modal + ConfirmDialog с opt-in
   defaultFocus='cancel'|'confirm'. ConfirmDialog по дефолту фокусит
   Cancel для destructive actions (safer чем Enter→Delete).
3. A11y фиксы:
   • text-slate-400→text-slate-500 в sidebar (contrast 2.63→4.61).
   • 8 страниц edit с back-arrow Link — aria-label + aria-hidden
     на иконке + текст-slate-500 цвет.
   • Modal close button — то же.
   • LoginPage — aria-invalid/aria-describedby/role=alert на
     ошибках валидации.
   • Field component — role="alert" на error span (announce'ит SR).
4. 8 файлов unit-тестов: PhoneNormalization, PagedRequest,
   RequiredGuid, RolePermissions (Domain), DomainPocoSmoke,
   DomainFullPropertyTouch, CatalogDtosSmoke, StockServiceProperty
   (4 seeds × 4 size + batch + 2-product isolation).
5. Backup-drill: pg_dump со stage'а → fresh postgres:16-alpine →
   pg_restore → dotnet run против восстановленной БД → /health/ready
   Healthy. Команды и timing в RUNBOOK.md.
6. Docs review:
   • MULTI-TENANCY чеклист «добавить tenant-сущность» расширен с 6
     до 19 шагов (Domain → EF Config → Migration с Xmin →
     RolePermissions → Validation → Controller + RequiresPermission →
     Audit + SensitiveOpsAudit → property tests).
   • ARCHITECTURE.md — Sprint 13-15 changes таблица.
   • DEVELOPER-GUIDE.md — «что добавилось после первого guide'а» +
     a11y pitfalls в «что НЕ делать».

Stage smoke ✓. Это финальный автономно-безопасный спринт. Дальше
нужен вход от user'а (ОФД keys, MoySklad tokens, Windows для POS,
прод-деплой план, kz-перевод, реальный SMTP).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 14:53:38 +05:00

136 lines
4.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FluentAssertions;
using foodmarket.Application.Catalog;
using foodmarket.Domain.Catalog;
using Xunit;
namespace foodmarket.UnitTests;
/// <summary>Sprint 15 — DTO/record-smoke. Records EF Core-friendly с
/// generated equality. Без минимального теста coverage по
/// Application.Catalog застревает на 0% (28 DTO).</summary>
public class CatalogDtosSmokeTests
{
private static readonly Guid Id = Guid.NewGuid();
[Fact]
public void Country_dto_round_trip()
{
var d = new CountryDto(Id, "KZ", "Казахстан", null, null, null, 12m);
d.Code.Should().Be("KZ");
d.VatRate.Should().Be(12m);
// record equality: same args → equal
new CountryDto(Id, "KZ", "Казахстан", null, null, null, 12m).Should().Be(d);
}
[Fact]
public void Currency_dto_round_trip()
{
var d = new CurrencyDto(Id, "KZT", "Тенге", "₸");
d.Symbol.Should().Be("₸");
}
[Fact]
public void UnitOfMeasure_dto_round_trip()
{
var d = new UnitOfMeasureDto(Id, "796", "штука", null);
d.IsActive.Should().BeTrue();
d.IsEnabledForOrg.Should().BeTrue();
var d2 = new UnitOfMeasureDto(Id, "796", "штука", null, false, false);
d2.IsActive.Should().BeFalse();
}
[Fact]
public void PriceType_dto_round_trip()
{
var d = new PriceTypeDto(Id, "Розничная", false, true, true, 0);
d.IsSystem.Should().BeTrue();
d.IsRetail.Should().BeTrue();
}
[Fact]
public void Store_dto_round_trip()
{
var d = new StoreDto(Id, "Главный", "MAIN", "Алматы", null, "Иванов", true, true);
d.IsMain.Should().BeTrue();
d.IsActive.Should().BeTrue();
}
[Fact]
public void RetailPoint_dto_round_trip()
{
var d = new RetailPointDto(Id, "Касса 1", "K1", Guid.NewGuid(), "Главный", null, null, null, null, true);
d.Name.Should().Be("Касса 1");
d.IsActive.Should().BeTrue();
}
[Fact]
public void ProductGroup_dto_round_trip()
{
var d = new ProductGroupDto(Id, "Хлеб", null, "/Хлеб", 0, 25m, null);
d.MarkupPercent.Should().Be(25m);
}
[Fact]
public void Counterparty_dto_round_trip()
{
var d = new CounterpartyDto(Id, "Партнёр", "ТОО Партнёр", CounterpartyType.LegalEntity,
"123", null, null, null, null, "Алматы", "+7 700", "x@y.kz",
"Каспи", "AS123", "BIK", "Сидоров", "ok");
d.Type.Should().Be(CounterpartyType.LegalEntity);
d.Bin.Should().Be("123");
}
[Fact]
public void Product_barcode_and_price_dtos()
{
var b = new ProductBarcodeDto(Id, "4607034521024", BarcodeType.Ean13, true);
b.IsPrimary.Should().BeTrue();
var p = new ProductPriceDto(Id, Guid.NewGuid(), "Розница", 250m, Guid.NewGuid(), "KZT");
p.Amount.Should().Be(250m);
}
[Fact]
public void Country_input_round_trip()
{
var i = new CountryInput("KZ", "Казахстан", Guid.NewGuid(), 12m);
i.Code.Should().Be("KZ");
i.VatRate.Should().Be(12m);
}
[Fact]
public void PriceType_input_round_trip()
{
var i = new PriceTypeInput("Опт", IsRetail: false, SortOrder: 10);
i.Name.Should().Be("Опт");
}
[Fact]
public void Store_input_round_trip()
{
var i = new StoreInput("Склад", "S1", Address: "Алматы", ManagerName: "Иванов", IsMain: false);
i.Name.Should().Be("Склад");
}
[Fact]
public void RetailPoint_input_round_trip()
{
var i = new RetailPointInput("Касса 1", "K1", Guid.NewGuid());
i.Name.Should().Be("Касса 1");
}
[Fact]
public void ProductGroup_input_round_trip()
{
var i = new ProductGroupInput("Хлеб", null, 0, 25m);
i.MarkupPercent.Should().Be(25m);
}
[Fact]
public void ProductBarcode_input_round_trip()
{
var i = new ProductBarcodeInput("4607034521024", BarcodeType.Ean13, true);
i.IsPrimary.Should().BeTrue();
}
}