using System.Net; using System.Text; using System.Text.Json.Nodes; using Deal.Ai.Llm; namespace Deal.Ai.Tests; // Снимок HTTP-запроса, записанный заглушкой обработчика (для проверок wire-контракта). // Url: Полный URL запроса. // Headers: Заголовки запроса (включая заголовки содержимого). // Body: Тело запроса (JSON-строка) либо null. internal sealed record CapturedHttpRequest( string Url, IReadOnlyDictionary Headers, string? Body); // Заглушка HttpMessageHandler для HTTP-тестов LlmHttpClient (план Task 7; без сети): записывает // запросы (URL/заголовки/тело) и отвечает по сценарию; опциональная задержка — для теста таймаута. internal sealed class StubHttpMessageHandler : HttpMessageHandler { private readonly Func _responder; private readonly TimeSpan? _delay; /// /// Создаёт заглушку с ответчиком сценария. /// /// Ответчик: request → response. /// Необязательная задержка ответа (тест таймаута). public StubHttpMessageHandler( Func responder, TimeSpan? delay = null) { _responder = responder; _delay = delay; } /// /// Запросы заглушки в порядке поступления. /// public List Requests { get; } = []; /// /// Последний запрос заглушки. /// public CapturedHttpRequest LastRequest => Requests[^1]; /// protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { string? body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken); var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair> header in request.Headers) { headers[header.Key] = string.Join(", ", header.Value); } if (request.Content?.Headers is { } contentHeaders) { foreach (KeyValuePair> header in contentHeaders) { headers[header.Key] = string.Join(", ", header.Value); } } Requests.Add(new CapturedHttpRequest( request.Method + " " + (request.RequestUri?.ToString() ?? string.Empty), headers, body)); if (_delay is { } delay) { await Task.Delay(delay, cancellationToken); } return _responder(request); } /// /// Ответчик: JSON-тело с HTTP 200. /// /// Тело ответа. public static Func JsonOk(string json) => _ => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json"), }; /// /// Ответчик: заданный HTTP-статус без тела. /// /// Код ответа. public static Func Status(HttpStatusCode statusCode) => _ => new HttpResponseMessage(statusCode); /// /// Разбирает тело запроса как JSON-объект (для проверок формы). /// /// Снимок запроса. public static JsonObject BodyOf(CapturedHttpRequest request) => JsonNode.Parse(request.Body!)!.AsObject(); /// /// Снимает заголовок авторизации Bearer (null — заголовка нет). /// /// Снимок запроса. public static string? BearerOf(CapturedHttpRequest request) => request.Headers.TryGetValue("Authorization", out string? value) ? value : null; }