- 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>
158 lines
9.1 KiB
C#
158 lines
9.1 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>Доработки сервера для 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);
|
||
}
|
||
}
|