using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
namespace foodmarket.IntegrationTests.Support;
/// Тонкая обёртка над HttpClient для интеграционных тестов: логин,
/// JSON-хелперы, выборка справочников орг-бутстрапа. Один экземпляр = один
/// «актор» (свой токен/орг).
public sealed class ApiActor
{
public HttpClient Http { get; }
public ApiActor(HttpClient http) => Http = http;
public void UseToken(string token)
=> Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// ── Auth ────────────────────────────────────────────────────────────────
public Task SignupAsync(string email, string password, string orgName, string phone = "+77001234567")
=> Http.PostAsJsonAsync("/api/auth/signup",
new { email, password, organizationName = orgName, phone, plan = (string?)null });
public async Task TokenAsync(string email, string password)
{
using var resp = await Http.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary
{
["grant_type"] = "password",
["username"] = email,
["password"] = password,
["client_id"] = "food-market-web",
["scope"] = "openid profile email roles api offline_access",
}));
resp.EnsureSuccessStatusCode();
return (await resp.Content.ReadFromJsonAsync()).GetProperty("access_token").GetString()!;
}
/// Регистрирует орг, логинит её администратора и проставляет токен.
/// Возвращает email/пароль администратора.
public async Task<(string Email, string Password)> SignupAndLoginAsync(string slug)
{
var email = $"{slug}@example.kz";
const string password = "Passw0rd!";
(await SignupAsync(email, password, $"Org {slug}")).EnsureSuccessStatusCode();
UseToken(await TokenAsync(email, password));
return (email, password);
}
// ── JSON helpers ──────────────────────────────────────────────────────────
public async Task GetJsonAsync(string url)
{
using var resp = await Http.GetAsync(url);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadFromJsonAsync();
}
/// items[] из PagedResult (или сам массив, если endpoint отдаёт список).
public async Task> ListAsync(string url)
{
var json = await GetJsonAsync(url);
var arr = json.ValueKind == JsonValueKind.Array ? json
: json.TryGetProperty("items", out var items) ? items
: default;
return arr.ValueKind == JsonValueKind.Array ? arr.EnumerateArray().ToList() : new();
}
public async Task FirstIdAsync(string url, Func? match = null)
{
var list = await ListAsync(url);
var hit = match is null ? list.FirstOrDefault() : list.FirstOrDefault(match);
return hit.ValueKind == JsonValueKind.Object ? hit.GetProperty("id").GetString()! : throw new InvalidOperationException($"No item in {url}");
}
// ── Org bootstrap refs ─────────────────────────────────────────────────────
public async Task LoadRefsAsync()
{
var unit = await FirstIdAsync("/api/catalog/units-of-measure?pageSize=50");
var group = await FirstIdAsync("/api/catalog/product-groups?pageSize=200");
var store = await FirstIdAsync("/api/catalog/stores?pageSize=200");
var currency = await FirstIdAsync("/api/catalog/currencies?pageSize=50",
e => e.GetProperty("code").GetString() == "KZT");
// Системный/розничный тип цены.
var priceType = await FirstIdAsync("/api/catalog/price-types?pageSize=200",
e => e.TryGetProperty("isSystem", out var s) && s.GetBoolean());
return new Refs(unit, group, store, currency, priceType);
}
public async Task CreateCounterpartyAsync(string name)
{
var resp = await Http.PostAsJsonAsync("/api/catalog/counterparties", new
{
name,
legalName = name,
type = 0,
bin = "987654321098",
iin = (string?)null,
taxNumber = (string?)null,
countryId = (string?)null,
address = "Алматы",
phone = "+77003332211",
email = "cp@example.kz",
bankName = (string?)null,
bankAccount = (string?)null,
bik = (string?)null,
contactPerson = "Иванов",
notes = (string?)null,
});
resp.EnsureSuccessStatusCode();
return (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!;
}
public async Task CreateProductAsync(Refs refs, string name, decimal retailPrice, string barcode)
{
var resp = await Http.PostAsJsonAsync("/api/catalog/products", new
{
name,
article = (string?)null,
description = (string?)null,
unitOfMeasureId = refs.UnitId,
vat = 12,
vatEnabled = true,
productGroupId = refs.GroupId,
defaultSupplierId = (string?)null,
countryOfOriginId = (string?)null,
isService = false,
packaging = 0,
isMarked = false,
minStock = (decimal?)null,
maxStock = (decimal?)null,
referencePrice = retailPrice * 0.7m,
purchaseCurrencyId = refs.CurrencyId,
imageUrl = (string?)null,
prices = new[] { new { priceTypeId = refs.PriceTypeId, amount = retailPrice, currencyId = refs.CurrencyId } },
barcodes = new[] { new { code = barcode, type = 0, isPrimary = true } },
});
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"CreateProduct failed {(int)resp.StatusCode}: {body}");
return JsonDocument.Parse(body).RootElement.GetProperty("id").GetString()!;
}
public async Task StockOfAsync(string storeId, string productId)
{
var list = await ListAsync($"/api/inventory/stock?productId={productId}&pageSize=200");
var row = list.FirstOrDefault(i =>
i.GetProperty("productId").GetString() == productId &&
i.GetProperty("storeId").GetString() == storeId);
return row.ValueKind == JsonValueKind.Object ? row.GetProperty("quantity").GetDecimal() : 0m;
}
public sealed record Refs(string UnitId, string GroupId, string StoreId, string CurrencyId, string PriceTypeId);
}