food-market/tests/food-market.IntegrationTests/SupplyPostUnpostTests.cs
nns f2dad91e05 test(integration): Testcontainers.PostgreSql + WebApplicationFactory, 10 тестов (P1-21)
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>
2026-05-27 03:14:01 +05:00

80 lines
3.8 KiB
C#
Raw Permalink 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.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);
}
}