< Summary

Information
Class: Services.ApiServices.MlAutoApi.MlAutoApiResponse
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MlAutoApi/MlAutoHttpClient.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 123
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Response()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MlAutoApi/MlAutoHttpClient.cs

#LineLine coverage
 1using System.Diagnostics;
 2using Newtonsoft.Json.Linq;
 3using Services.ApiServices.ApiSupplierRequestLog;
 4
 5namespace Services.ApiServices.MlAutoApi;
 6
 2367public sealed record MlAutoApiResponse(Newtonsoft.Json.Linq.JToken Response, string? Message);
 8
 9/// <summary>Read-only клиент ML-Auto: API принимает только form-urlencoded POST.</summary>
 10public class MlAutoHttpClient
 11{
 12    private readonly IHttpClientFactory _factory;
 13    private readonly MlAutoConfig _config;
 14    private readonly ApiSupplierRequestLogStore? _logs;
 15    private readonly string _httpClientName;
 16    private readonly string _providerKey;
 17    private readonly MlAutoRateLimiter _limiter;
 18
 19    public MlAutoHttpClient(IHttpClientFactory factory, MlAutoConfig config, ApiSupplierRequestLogStore? logs = null,
 20        string httpClientName = "MlAutoApi", string providerKey = "MlAuto")
 21    {
 22        _factory = factory;
 23        _config = config;
 24        _logs = logs;
 25        _httpClientName = httpClientName;
 26        _providerKey = providerKey;
 27        _limiter = new MlAutoRateLimiter(config.RequestsPerMinute);
 28    }
 29
 30    public async Task<JToken> PingAsync(CancellationToken cancellationToken = default) =>
 31        (await PostDetailedAsync("/Ping/", null, false, cancellationToken)).Response;
 32
 33    public async Task<JToken> PostAsync(string path, IReadOnlyDictionary<string, string?>? parameters, CancellationToken
 34        (await PostDetailedAsync(path, parameters, true, cancellationToken)).Response;
 35
 36    public Task<MlAutoApiResponse> PostDetailedAsync(string path, IReadOnlyDictionary<string, string?>? parameters, Canc
 37        PostDetailedAsync(path, parameters, true, cancellationToken);
 38
 39    private async Task<MlAutoApiResponse> PostDetailedAsync(string path, IReadOnlyDictionary<string, string?>? parameter
 40    {
 41        if (authorize && (string.IsNullOrWhiteSpace(_config.Login) || string.IsNullOrWhiteSpace(_config.Password)))
 42            throw new MlAutoApiException("Не настроены MlAuto:Login и/или MlAuto:Password.");
 43
 44        var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 45        if (authorize)
 46        {
 47            values["LOGIN"] = _config.Login;
 48            values["PASSWORD"] = _config.Password;
 49        }
 50        if (parameters != null)
 51            foreach (var (key, value) in parameters.Where(x => !string.IsNullOrWhiteSpace(x.Value)))
 52                values[key] = value!;
 53
 54        await _limiter.WaitAsync(cancellationToken);
 55        var timer = Stopwatch.StartNew();
 56        HttpResponseMessage response;
 57        string text;
 58        try
 59        {
 60            response = await _factory.CreateClient(_httpClientName).PostAsync(BuildUri(path), new FormUrlEncodedContent(
 61            text = await response.Content.ReadAsStringAsync(cancellationToken);
 62        }
 63        catch (Exception ex)
 64        {
 65            _logs?.Add(_providerKey, "POST", path, null, timer,
 66                string.Join("&", values.Select(x => $"{x.Key}={x.Value}")), ex.Message);
 67            throw;
 68        }
 69        using (response)
 70        {
 71        _logs?.Add(_providerKey, "POST", path, (int)response.StatusCode, timer,
 72            string.Join("&", values.Select(x => $"{x.Key}={x.Value}")), text);
 73        if (!response.IsSuccessStatusCode)
 74            throw new MlAutoApiException($"ML-Auto вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, p
 75        }
 76
 77        JObject envelope;
 78        try { envelope = JObject.Parse(text); }
 79        catch (Exception ex)
 80        {
 81            var snippet = FormatResponseSnippet(text);
 82            var message = string.IsNullOrWhiteSpace(snippet)
 83                ? "ML-Auto вернул невалидный JSON."
 84                : $"ML-Auto вернул невалидный JSON (ответ: '{snippet}').";
 85            throw new MlAutoApiException(message, requestPath: path, innerException: ex);
 86        }
 87
 88        var providerMessage = GetProviderMessage(envelope["MESSAGES"]);
 89        var status = envelope.Value<int?>("STATUS") ?? 200;
 90        if (status != 200)
 91        {
 92            var rateLimited = IsRateLimitMessage(providerMessage);
 93            if (rateLimited) _limiter.Penalize();
 94            throw new MlAutoApiException(string.IsNullOrWhiteSpace(providerMessage) ? $"ML-Auto вернул статус {status}."
 95            {
 96                IsRateLimited = rateLimited
 97            };
 98        }
 99        return new MlAutoApiResponse(envelope["RESPONSE"] ?? new JArray(), string.IsNullOrWhiteSpace(providerMessage) ? 
 100    }
 101
 102    private static bool IsRateLimitMessage(string? message) =>
 103        message?.Contains("лимит запросов", StringComparison.OrdinalIgnoreCase) == true;
 104
 105    private static string GetProviderMessage(JToken? messages) => messages switch
 106    {
 107        JArray array => string.Join("; ", array.Select(ExtractMessage).Where(x => !string.IsNullOrWhiteSpace(x))),
 108        JObject obj => string.Join("; ", obj.Properties().Select(x => ExtractMessage(x.Value)).Where(x => !string.IsNull
 109        _ => string.Empty
 110    };
 111
 112    private static string ExtractMessage(JToken message) =>
 113        message is JObject obj ? obj.Value<string>("MSG_TEXT") ?? obj.ToString() : message.ToString();
 114
 115    private static string FormatResponseSnippet(string? text)
 116    {
 117        if (string.IsNullOrWhiteSpace(text)) return string.Empty;
 118        var singleLine = string.Join(" ", text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
 119        return singleLine.Length > 120 ? singleLine[..117] + "..." : singleLine;
 120    }
 121
 122    private Uri BuildUri(string path) => new(_config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/'));
 123}

Methods/Properties

get_Response()