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 MetricsEndpointTests
{
private readonly ApiFactory _factory;
public MetricsEndpointTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
/// /metrics доступен без авторизации (стандарт Prometheus) и
/// выдаёт text/plain в exposition format.
[Fact]
public async Task Metrics_endpoint_unauthenticated_returns_prometheus_text()
{
using var client = _factory.CreateClient();
using var resp = await client.GetAsync("/metrics");
resp.IsSuccessStatusCode.Should().BeTrue($"/metrics вернул {(int)resp.StatusCode}");
resp.Content.Headers.ContentType!.MediaType.Should().StartWith("text/plain");
var text = await resp.Content.ReadAsStringAsync();
// process_* + dotnet_* — встроенные коллекторы prometheus-net.
text.Should().Contain("process_cpu_seconds_total");
// HTTP-метрика появляется после первого UseHttpMetrics-обработанного запроса
// (этот же GET /metrics на пути HTTP-pipeline уже учитан в счётчике).
text.Should().Contain("http_requests_received_total");
}
/// После проведения чека счётчик sales_posted увеличивается.
[Fact]
public async Task Sales_posted_counter_increments_after_post()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"metr-sale-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
// Подкормка склада, чек, проводим.
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 = p1, quantity = 5m, unitCost = 50m } },
});
enter.EnsureSuccessStatusCode();
var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
// Снимаем "до".
var before = await ReadMetricSample("food_market_sales_posted_total", null);
var sale = await api.Http.PostAsJsonAsync("/api/sales/retail", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, retailPointId = (string?)null,
customerId = (string?)null, currencyId = refs.CurrencyId, payment = 0, paidCash = 100m, paidCard = 0m,
lines = new[] { new { productId = p1, quantity = 1m, unitPrice = 100m, discount = 0m, vatPercent = 12m } },
notes = "sale",
});
sale.EnsureSuccessStatusCode();
var sid = (await sale.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/sales/retail/{sid}/post", new { })).EnsureSuccessStatusCode();
var after = await ReadMetricSample("food_market_sales_posted_total", null);
after.Should().BeGreaterThan(before);
}
/// DB-длительность измеряется через интерсептор EF.
[Fact]
public async Task Db_query_duration_metric_observed()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"metr-db-{Guid.NewGuid():N}");
// Любой запрос с EF в обработчике (signup уже сделал SELECT/INSERT'ы).
await api.GetJsonAsync("/api/catalog/products?pageSize=10");
using var client = _factory.CreateClient();
var text = await client.GetStringAsync("/metrics");
text.Should().Contain("food_market_db_query_duration_seconds_count");
}
/// Читает значение sample'а метрики (тип counter без labels).
/// Возвращает 0 если строки нет.
private async Task ReadMetricSample(string metricName, string? label)
{
using var client = _factory.CreateClient();
var text = await client.GetStringAsync("/metrics");
var prefix = label is null ? metricName : metricName;
foreach (var line in text.Split('\n'))
{
if (line.StartsWith("#")) continue;
if (!line.StartsWith(prefix)) continue;
// Формат: "metric_name{labels} value timestamp?" или "metric_name value"
var parts = line.Trim().Split(' ');
if (parts.Length >= 2 && double.TryParse(parts[^1], System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var v))
return v;
}
return 0;
}
}