Some checks failed
Auto-tag / Create date-tag (push) Has been cancelled
CI / Backend (.NET 8) (push) Has been cancelled
CI / Web (React + Vite) (push) Has been cancelled
CI / POS (WPF, Windows) (push) Has been cancelled
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Старый idempotency-маркер — Product с Article LIKE 'DEMO-%'. После того как «Демо-магазин» на стенде наполнили реальным каталогом из МойСклада (артикулы не DEMO-*), маркер срабатывал false и QuickLoginDemoSeeder при каждом рестарте стенда пытался залить фиктивный каталог заново — падая на unique-индексе IX_product_barcodes_OrganizationId_Code (23505). В БД ничего лишнего не попадало (транзакция откатывалась), но лог stage-api получал 9 ERR в сутки. Фикс: SeedAsync и PeekAsync считают «уже сидировано» при любом Product в орге. Поведение из UI (POST /api/admin/seed-demo) не меняется для свежих org'ов. Прочие стартовые сидеры (OpenIddictClientSeeder, SystemReferenceSeeder, DevDataSeeder) и HangfireJobsConfigurator — уже идемпотентны, аудит в docs/seeder-noise-progress.md. Integration-тест: - SeedDemo_second_call_on_populated_org_returns_already_seeded - SeedDemo_on_org_with_imported_catalog_skips_without_duplicate_barcode Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
74 lines
4.5 KiB
C#
74 lines
4.5 KiB
C#
using System.Net;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using FluentAssertions;
|
||
using foodmarket.IntegrationTests.Support;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.IntegrationTests;
|
||
|
||
/// <summary>Регрессия на шум в логах stage-api: раньше
|
||
/// <see cref="foodmarket.Api.Seed.DemoTenantSeeder"/> определял «уже сидировано»
|
||
/// по наличию Product с Article LIKE 'DEMO-%'. После того как «Демо-магазин»
|
||
/// на стенде наполнили реальным каталогом из МойСклада (не с префиксом DEMO-),
|
||
/// маркер срабатывал false — сидер шёл заново и падал на unique-индексе
|
||
/// IX_product_barcodes_OrganizationId_Code (23505) при каждом рестарте.
|
||
/// Фикс расширил маркер до «любой Product в орге». Тесты фиксируют:
|
||
/// 1) повторный Seed на орге с товарами не пишет и не падает;
|
||
/// 2) org с собственным (не-DEMO) каталогом тоже защищена.</summary>
|
||
[Collection(ApiCollection.Name)]
|
||
public class DemoSeederIdempotencyTests
|
||
{
|
||
private readonly ApiFactory _factory;
|
||
public DemoSeederIdempotencyTests(ApiFactory factory) => _factory = factory;
|
||
|
||
[Fact]
|
||
public async Task SeedDemo_second_call_on_populated_org_returns_already_seeded()
|
||
{
|
||
// Фоновый сценарий: Admin впервые вызвал seed-demo (50 товаров DEMO-*),
|
||
// перезапуск или повторное нажатие не должны дёрнуть вставку второй раз.
|
||
var a = new ApiActor(_factory.CreateClient());
|
||
await a.SignupAndLoginAsync($"seed-twice-{Guid.NewGuid():N}");
|
||
|
||
using var first = await a.Http.PostAsync("/api/admin/seed-demo", content: null);
|
||
first.StatusCode.Should().Be(HttpStatusCode.OK);
|
||
var firstBody = await first.Content.ReadFromJsonAsync<JsonElement>();
|
||
firstBody.GetProperty("alreadySeeded").GetBoolean().Should().BeFalse();
|
||
firstBody.GetProperty("products").GetInt32().Should().Be(50);
|
||
|
||
using var second = await a.Http.PostAsync("/api/admin/seed-demo", content: null);
|
||
second.StatusCode.Should().Be(HttpStatusCode.OK);
|
||
var secondBody = await second.Content.ReadFromJsonAsync<JsonElement>();
|
||
secondBody.GetProperty("alreadySeeded").GetBoolean().Should().BeTrue();
|
||
// Количество товаров не выросло — повторная вставка не произошла.
|
||
secondBody.GetProperty("products").GetInt32().Should().Be(50);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task SeedDemo_on_org_with_imported_catalog_skips_without_duplicate_barcode()
|
||
{
|
||
// Главная регрессия: воспроизводит stage-сценарий, где у организации уже
|
||
// есть товары БЕЗ префикса DEMO-. Старый маркер (Article LIKE 'DEMO-%')
|
||
// пропускал гард и сидер падал на unique-индексе по баркодам (23505).
|
||
var a = new ApiActor(_factory.CreateClient());
|
||
await a.SignupAndLoginAsync($"seed-imported-{Guid.NewGuid():N}");
|
||
var refs = await a.LoadRefsAsync();
|
||
|
||
// Один товар с «импортным» (не DEMO-*) артикулом — имитирует каталог
|
||
// из МойСклада. Этого хватает чтобы новый маркер «any Product» сработал.
|
||
await a.CreateProductAsync(refs, $"ImportedProduct-{Guid.NewGuid():N}",
|
||
retailPrice: 500m, barcode: $"IMP-{Guid.NewGuid():N}");
|
||
|
||
using var resp = await a.Http.PostAsync("/api/admin/seed-demo", content: null);
|
||
resp.StatusCode.Should().Be(HttpStatusCode.OK, "сидер должен no-op'нуть, а не упасть с 500");
|
||
var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||
body.GetProperty("alreadySeeded").GetBoolean().Should().BeTrue();
|
||
// Должен быть ровно 1 товар — тот что мы создали; никаких DEMO- вставок.
|
||
body.GetProperty("products").GetInt32().Should().Be(1);
|
||
|
||
// Status (Peek) тоже отражает «заселено» — чтобы UI не предлагал кнопку.
|
||
var status = await a.GetJsonAsync("/api/admin/seed-demo/status");
|
||
status.GetProperty("alreadySeeded").GetBoolean().Should().BeTrue();
|
||
}
|
||
}
|