ApiFactory поднимает реальный API на одноразовом postgres:16-alpine (Ryuk off — сеть к Docker Hub нестабильна, образ закэширован; RateLimiting off через env, т.к. лимитер читает конфиг эагерно). Program сделан public partial для фабрики. Сценарии (10 зелёных): - signup-flow: signup→token→/api/me с org; дубль-signup 400; слабый пароль 400. - tenant isolation A vs B: контрагент A не виден B (список + прямой GET 404). - permission: кастомная роль без ProductsEdit → PUT товара 403, GET 200; админ не 403. - supply post→unpost: остаток 0→10, Cost=70 (скользящее среднее), unpost→0; двойной post 409. - retail overselling: продажа сверх остатка → 409; недоплата → 400. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
3.8 KiB
C#
80 lines
3.8 KiB
C#
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using FluentAssertions;
|
||
using foodmarket.IntegrationTests.Support;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.IntegrationTests;
|
||
|
||
[Collection(ApiCollection.Name)]
|
||
public class SupplyPostUnpostTests
|
||
{
|
||
private readonly ApiFactory _factory;
|
||
public SupplyPostUnpostTests(ApiFactory factory) => _factory = factory;
|
||
|
||
private static string RandomBarcode()
|
||
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
|
||
|
||
[Fact]
|
||
public async Task Posting_supply_raises_stock_and_sets_cost_unpost_reverts()
|
||
{
|
||
var api = new ApiActor(_factory.CreateClient());
|
||
await api.SignupAndLoginAsync($"supply-{Guid.NewGuid():N}");
|
||
var refs = await api.LoadRefsAsync();
|
||
var supplierId = await api.CreateCounterpartyAsync($"Supplier-{Guid.NewGuid():N}");
|
||
var productId = await api.CreateProductAsync(refs, $"Prod-{Guid.NewGuid():N}", retailPrice: 150m, RandomBarcode());
|
||
|
||
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(0m, "новый товар без приёмок");
|
||
|
||
// Draft supply: 10 шт по 70.
|
||
var draftResp = await api.Http.PostAsJsonAsync("/api/purchases/supplies", new
|
||
{
|
||
date = DateTime.UtcNow,
|
||
supplierId,
|
||
storeId = refs.StoreId,
|
||
currencyId = refs.CurrencyId,
|
||
notes = "integration",
|
||
lines = new[] { new { productId, quantity = 10m, unitPrice = 70m } },
|
||
});
|
||
draftResp.EnsureSuccessStatusCode();
|
||
var supplyId = (await draftResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
|
||
|
||
// Post → остаток 10, себестоимость 70 (скользящее среднее с нуля).
|
||
using var post = await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supplyId}/post", new { });
|
||
post.IsSuccessStatusCode.Should().BeTrue($"post вернул {(int)post.StatusCode}: {await post.Content.ReadAsStringAsync()}");
|
||
|
||
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(10m);
|
||
var product = await api.GetJsonAsync($"/api/catalog/products/{productId}");
|
||
product.GetProperty("cost").GetDecimal().Should().Be(70m);
|
||
|
||
// Unpost → остаток возвращается к 0.
|
||
using var unpost = await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supplyId}/unpost", new { });
|
||
unpost.IsSuccessStatusCode.Should().BeTrue($"unpost вернул {(int)unpost.StatusCode}: {await unpost.Content.ReadAsStringAsync()}");
|
||
|
||
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(0m);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Double_post_is_rejected()
|
||
{
|
||
var api = new ApiActor(_factory.CreateClient());
|
||
await api.SignupAndLoginAsync($"dblpost-{Guid.NewGuid():N}");
|
||
var refs = await api.LoadRefsAsync();
|
||
var supplierId = await api.CreateCounterpartyAsync($"Supplier-{Guid.NewGuid():N}");
|
||
var productId = await api.CreateProductAsync(refs, $"Prod-{Guid.NewGuid():N}", 150m, RandomBarcode());
|
||
|
||
var draftResp = await api.Http.PostAsJsonAsync("/api/purchases/supplies", new
|
||
{
|
||
date = DateTime.UtcNow, supplierId, storeId = refs.StoreId, currencyId = refs.CurrencyId,
|
||
notes = "dbl", lines = new[] { new { productId, quantity = 3m, unitPrice = 50m } },
|
||
});
|
||
draftResp.EnsureSuccessStatusCode();
|
||
var supplyId = (await draftResp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
|
||
|
||
(await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supplyId}/post", new { }))
|
||
.IsSuccessStatusCode.Should().BeTrue();
|
||
using var second = await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supplyId}/post", new { });
|
||
((int)second.StatusCode).Should().Be(409);
|
||
}
|
||
}
|