Files
Deal/src/ai-service/Deal.Ai.Tests/StubHttpMessageHandler.cs
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

114 lines
4.6 KiB
C#
Raw 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;
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<string, string> Headers,
string? Body);
// Заглушка HttpMessageHandler для HTTP-тестов LlmHttpClient (план Task 7; без сети): записывает
// запросы (URL/заголовки/тело) и отвечает по сценарию; опциональная задержка — для теста таймаута.
internal sealed class StubHttpMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage> _responder;
private readonly TimeSpan? _delay;
/// <summary>
/// Создаёт заглушку с ответчиком сценария.
/// </summary>
/// <param name="responder">Ответчик: request → response.</param>
/// <param name="delay">Необязательная задержка ответа (тест таймаута).</param>
public StubHttpMessageHandler(
Func<HttpRequestMessage, HttpResponseMessage> responder,
TimeSpan? delay = null)
{
_responder = responder;
_delay = delay;
}
/// <summary>
/// Запросы заглушки в порядке поступления.
/// </summary>
public List<CapturedHttpRequest> Requests { get; } = [];
/// <summary>
/// Последний запрос заглушки.
/// </summary>
public CapturedHttpRequest LastRequest => Requests[^1];
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
string? body = request.Content is null
? null
: await request.Content.ReadAsStringAsync(cancellationToken);
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
headers[header.Key] = string.Join(", ", header.Value);
}
if (request.Content?.Headers is { } contentHeaders)
{
foreach (KeyValuePair<string, IEnumerable<string>> 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);
}
/// <summary>
/// Ответчик: JSON-тело с HTTP 200.
/// </summary>
/// <param name="json">Тело ответа.</param>
public static Func<HttpRequestMessage, HttpResponseMessage> JsonOk(string json)
=> _ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json"),
};
/// <summary>
/// Ответчик: заданный HTTP-статус без тела.
/// </summary>
/// <param name="statusCode">Код ответа.</param>
public static Func<HttpRequestMessage, HttpResponseMessage> Status(HttpStatusCode statusCode)
=> _ => new HttpResponseMessage(statusCode);
/// <summary>
/// Разбирает тело запроса как JSON-объект (для проверок формы).
/// </summary>
/// <param name="request">Снимок запроса.</param>
public static JsonObject BodyOf(CapturedHttpRequest request)
=> JsonNode.Parse(request.Body!)!.AsObject();
/// <summary>
/// Снимает заголовок авторизации Bearer (null — заголовка нет).
/// </summary>
/// <param name="request">Снимок запроса.</param>
public static string? BearerOf(CapturedHttpRequest request)
=> request.Headers.TryGetValue("Authorization", out string? value) ? value : null;
}