food-market/tests/food-market.IntegrationTests/DemoSeederIdempotencyTests.cs
nns 99bf6c9f3e
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
fix(seed): DemoTenantSeeder гардит по наличию ЛЮБЫХ товаров в орге
Старый 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>
2026-10-06 00:50:50 +05:00

74 lines
4.5 KiB
C#
Raw 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 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();
}
}