food-market/tests/food-market.IntegrationTests/MobileApiTests.cs
nns 14001069a2 test(mobile-api): unit + integration доработок для «FM Склад»
- unit: WeightedBarcode (префиксы/код/вес/цена/настройки) + RolePermissions.Effective
  (SuperAdmin/Admin/роль × простой/расширенный) — 10 тестов.
- integration (Testcontainers): фильтр остатков по вложенной группе; добавление
  штрихкода (201/идемпотентно/409 дубль/400); /api/me permissions; весовая этикетка
  в by-barcode (товар + количество) — 4 теста.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 05:39:37 +05:00

158 lines
9.1 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>Доработки сервера для iOS «FM Склад»: фильтр остатков по группе
/// (вложенной), добавление штрихкода, права в /api/me, весовые этикетки.</summary>
[Collection(ApiCollection.Name)]
public class MobileApiTests
{
private readonly ApiFactory _factory;
public MobileApiTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
private static async Task<string> CreateGroupAsync(ApiActor api, string name, string? parentId)
{
var resp = await api.Http.PostAsJsonAsync("/api/catalog/product-groups", new { name, parentId });
var body = await resp.Content.ReadAsStringAsync();
resp.IsSuccessStatusCode.Should().BeTrue($"create group: {(int)resp.StatusCode} {body}");
return JsonDocument.Parse(body).RootElement.GetProperty("id").GetString()!;
}
private static async Task<string> CreateProductInGroupAsync(
ApiActor api, ApiActor.Refs refs, string name, decimal price, string barcode, string groupId)
{
var resp = await api.Http.PostAsJsonAsync("/api/catalog/products", new
{
name, article = (string?)null, unitOfMeasureId = refs.UnitId, vat = 12, vatEnabled = true,
productGroupId = groupId, isService = false, packaging = 0, isMarked = false,
referencePrice = price * 0.7m, purchaseCurrencyId = refs.CurrencyId,
prices = new[] { new { priceTypeId = refs.PriceTypeId, amount = price, currencyId = refs.CurrencyId } },
barcodes = new[] { new { code = barcode, type = 0, isPrimary = true } },
});
var body = await resp.Content.ReadAsStringAsync();
resp.IsSuccessStatusCode.Should().BeTrue($"create product: {(int)resp.StatusCode} {body}");
return JsonDocument.Parse(body).RootElement.GetProperty("id").GetString()!;
}
private static async Task SeedStockAsync(ApiActor api, ApiActor.Refs refs, string productId, decimal qty)
{
var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed", lines = new[] { new { productId, quantity = qty, unitCost = 50m } },
});
enter.EnsureSuccessStatusCode();
var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
}
// ── 1. Фильтр остатков по группе (с вложенными) ────────────────────────
[Fact]
public async Task Stock_filter_by_group_includes_nested_groups()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"mob-grp-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var parent = await CreateGroupAsync(api, $"Напитки-{Guid.NewGuid():N}", null);
var child = await CreateGroupAsync(api, $"Соки-{Guid.NewGuid():N}", parent);
var pInParent = await CreateProductInGroupAsync(api, refs, $"Вода-{Guid.NewGuid():N}", 100m, RandomBarcode(), parent);
var pInChild = await CreateProductInGroupAsync(api, refs, $"Сок-{Guid.NewGuid():N}", 200m, RandomBarcode(), child);
await SeedStockAsync(api, refs, pInParent, 5m);
await SeedStockAsync(api, refs, pInChild, 7m);
// Фильтр по родителю → оба товара (вложенность).
var byParent = await api.Http.GetFromJsonAsync<JsonElement>($"/api/inventory/stock?groupId={parent}&pageSize=100");
var parentIds = byParent.GetProperty("items").EnumerateArray()
.Select(x => x.GetProperty("productId").GetString()).ToHashSet();
parentIds.Should().Contain(pInParent);
parentIds.Should().Contain(pInChild, "вложенная группа входит в фильтр родителя");
// Фильтр по дочерней → только её товар.
var byChild = await api.Http.GetFromJsonAsync<JsonElement>($"/api/inventory/stock?groupId={child}&pageSize=100");
var childIds = byChild.GetProperty("items").EnumerateArray()
.Select(x => x.GetProperty("productId").GetString()).ToHashSet();
childIds.Should().Contain(pInChild);
childIds.Should().NotContain(pInParent);
}
// ── 2. Добавление штрихкода к товару ───────────────────────────────────
[Fact]
public async Task Add_barcode_creates_and_rejects_duplicate()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"mob-bc-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"A-{Guid.NewGuid():N}", 100m, RandomBarcode());
var p2 = await api.CreateProductAsync(refs, $"B-{Guid.NewGuid():N}", 100m, RandomBarcode());
var newCode = RandomBarcode();
// Добавляем новый код к p1.
using var add = await api.Http.PostAsJsonAsync($"/api/catalog/products/{p1}/barcodes", new { code = newCode, kind = "ean13" });
add.StatusCode.Should().Be(HttpStatusCode.Created);
var created = await add.Content.ReadFromJsonAsync<JsonElement>();
created.GetProperty("code").GetString().Should().Be(newCode);
created.GetProperty("type").GetString().Should().Be("Ean13");
// Повтор того же кода тому же товару → идемпотентно 200.
using var again = await api.Http.PostAsJsonAsync($"/api/catalog/products/{p1}/barcodes", new { code = newCode, kind = (string?)null });
again.StatusCode.Should().Be(HttpStatusCode.OK);
// Тот же код другому товару → 409 с названием владельца.
using var dup = await api.Http.PostAsJsonAsync($"/api/catalog/products/{p2}/barcodes", new { code = newCode, kind = (string?)null });
dup.StatusCode.Should().Be(HttpStatusCode.Conflict);
var err = await dup.Content.ReadFromJsonAsync<JsonElement>();
err.GetProperty("code").GetString().Should().Be("barcode-duplicate");
err.GetProperty("error").GetString().Should().Contain("уже у товара");
// Пустой код → 400.
using var bad = await api.Http.PostAsJsonAsync($"/api/catalog/products/{p1}/barcodes", new { code = "", kind = (string?)null });
bad.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
// ── 3. Права текущего пользователя в /api/me ───────────────────────────
[Fact]
public async Task Me_returns_effective_permissions()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"mob-me-{Guid.NewGuid():N}");
var me = await api.Http.GetFromJsonAsync<JsonElement>("/api/me");
me.GetProperty("roles").EnumerateArray().Select(r => r.GetString()).Should().Contain("Admin");
var perms = me.GetProperty("permissions");
perms.GetProperty("ProductsEdit").GetBoolean().Should().BeTrue();
perms.GetProperty("StocksView").GetBoolean().Should().BeTrue();
// Новая орг по умолчанию в простом режиме → advanced-only скрыты даже у Admin.
me.GetProperty("advancedMode").GetBoolean().Should().BeFalse();
perms.GetProperty("DemandsView").GetBoolean().Should().BeFalse();
perms.GetProperty("TransferEdit").GetBoolean().Should().BeFalse();
}
// ── 5. Весовые этикетки в поиске по штрихкоду ──────────────────────────
[Fact]
public async Task ByBarcode_parses_weighted_label_returns_product_and_quantity()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"mob-w-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
// PLU = 12345 хранится как короткий штрихкод товара.
var pid = await api.CreateProductAsync(refs, $"Яблоки-{Guid.NewGuid():N}", 690m, "12345");
// Весовая этикетка: 21 | 12345 | 01500 | K → вес 1.500 кг.
var resp = await api.Http.GetFromJsonAsync<JsonElement>("/api/catalog/products/by-barcode/2112345015007");
resp.GetProperty("weighted").GetBoolean().Should().BeTrue();
resp.GetProperty("valueKind").GetString().Should().Be("WeightKg");
resp.GetProperty("quantity").GetDecimal().Should().Be(1.500m);
resp.GetProperty("product").GetProperty("id").GetString().Should().Be(pid);
}
}