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); }