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>
136 lines
4.1 KiB
C#
136 lines
4.1 KiB
C#
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();
|
||
}
|
||
}
|