Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
114 lines
4.6 KiB
C#
114 lines
4.6 KiB
C#
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;
|
||
}
|