< Summary

Information
Class: Services.ApiServices.MlAutoApi.MlAutoHttpClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MlAutoApi/MlAutoHttpClient.cs
Line coverage
96%
Covered lines: 64
Uncovered lines: 2
Coverable lines: 66
Total lines: 123
Line coverage: 96.9%
Branch coverage
75%
Covered branches: 33
Total branches: 44
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
PingAsync()100%210%
PostAsync()100%11100%
PostDetailedAsync(...)100%11100%
PostDetailedAsync()84.37%323297.43%
IsRateLimitMessage(...)50%22100%
GetProviderMessage(...)100%44100%
ExtractMessage(...)25%44100%
FormatResponseSnippet(...)50%44100%
BuildUri(...)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
 7public 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
 11219    public MlAutoHttpClient(IHttpClientFactory factory, MlAutoConfig config, ApiSupplierRequestLogStore? logs = null,
 11220        string httpClientName = "MlAutoApi", string providerKey = "MlAuto")
 21    {
 11222        _factory = factory;
 11223        _config = config;
 11224        _logs = logs;
 11225        _httpClientName = httpClientName;
 11226        _providerKey = providerKey;
 11227        _limiter = new MlAutoRateLimiter(config.RequestsPerMinute);
 11228    }
 29
 30    public async Task<JToken> PingAsync(CancellationToken cancellationToken = default) =>
 031        (await PostDetailedAsync("/Ping/", null, false, cancellationToken)).Response;
 32
 33    public async Task<JToken> PostAsync(string path, IReadOnlyDictionary<string, string?>? parameters, CancellationToken
 9134        (await PostDetailedAsync(path, parameters, true, cancellationToken)).Response;
 35
 36    public Task<MlAutoApiResponse> PostDetailedAsync(string path, IReadOnlyDictionary<string, string?>? parameters, Canc
 2037        PostDetailedAsync(path, parameters, true, cancellationToken);
 38
 39    private async Task<MlAutoApiResponse> PostDetailedAsync(string path, IReadOnlyDictionary<string, string?>? parameter
 40    {
 11141        if (authorize && (string.IsNullOrWhiteSpace(_config.Login) || string.IsNullOrWhiteSpace(_config.Password)))
 042            throw new MlAutoApiException("Не настроены MlAuto:Login и/или MlAuto:Password.");
 43
 11144        var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 11145        if (authorize)
 46        {
 11147            values["LOGIN"] = _config.Login;
 11148            values["PASSWORD"] = _config.Password;
 49        }
 11150        if (parameters != null)
 147751            foreach (var (key, value) in parameters.Where(x => !string.IsNullOrWhiteSpace(x.Value)))
 42252                values[key] = value!;
 53
 11154        await _limiter.WaitAsync(cancellationToken);
 11055        var timer = Stopwatch.StartNew();
 56        HttpResponseMessage response;
 57        string text;
 58        try
 59        {
 11060            response = await _factory.CreateClient(_httpClientName).PostAsync(BuildUri(path), new FormUrlEncodedContent(
 10661            text = await response.Content.ReadAsStringAsync(cancellationToken);
 10662        }
 463        catch (Exception ex)
 64        {
 465            _logs?.Add(_providerKey, "POST", path, null, timer,
 1066                string.Join("&", values.Select(x => $"{x.Key}={x.Value}")), ex.Message);
 467            throw;
 68        }
 10669        using (response)
 70        {
 10671        _logs?.Add(_providerKey, "POST", path, (int)response.StatusCode, timer,
 65272            string.Join("&", values.Select(x => $"{x.Key}={x.Value}")), text);
 10673        if (!response.IsSuccessStatusCode)
 174            throw new MlAutoApiException($"ML-Auto вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, p
 10575        }
 76
 77        JObject envelope;
 20978        try { envelope = JObject.Parse(text); }
 179        catch (Exception ex)
 80        {
 181            var snippet = FormatResponseSnippet(text);
 182            var message = string.IsNullOrWhiteSpace(snippet)
 183                ? "ML-Auto вернул невалидный JSON."
 184                : $"ML-Auto вернул невалидный JSON (ответ: '{snippet}').";
 185            throw new MlAutoApiException(message, requestPath: path, innerException: ex);
 86        }
 87
 10488        var providerMessage = GetProviderMessage(envelope["MESSAGES"]);
 10489        var status = envelope.Value<int?>("STATUS") ?? 200;
 10490        if (status != 200)
 91        {
 592            var rateLimited = IsRateLimitMessage(providerMessage);
 693            if (rateLimited) _limiter.Penalize();
 594            throw new MlAutoApiException(string.IsNullOrWhiteSpace(providerMessage) ? $"ML-Auto вернул статус {status}."
 595            {
 596                IsRateLimited = rateLimited
 597            };
 98        }
 9999        return new MlAutoApiResponse(envelope["RESPONSE"] ?? new JArray(), string.IsNullOrWhiteSpace(providerMessage) ? 
 99100    }
 101
 102    private static bool IsRateLimitMessage(string? message) =>
 5103        message?.Contains("лимит запросов", StringComparison.OrdinalIgnoreCase) == true;
 104
 104105    private static string GetProviderMessage(JToken? messages) => messages switch
 104106    {
 14107        JArray array => string.Join("; ", array.Select(ExtractMessage).Where(x => !string.IsNullOrWhiteSpace(x))),
 6108        JObject obj => string.Join("; ", obj.Properties().Select(x => ExtractMessage(x.Value)).Where(x => !string.IsNull
 93109        _ => string.Empty
 104110    };
 111
 112    private static string ExtractMessage(JToken message) =>
 7113        message is JObject obj ? obj.Value<string>("MSG_TEXT") ?? obj.ToString() : message.ToString();
 114
 115    private static string FormatResponseSnippet(string? text)
 116    {
 1117        if (string.IsNullOrWhiteSpace(text)) return string.Empty;
 1118        var singleLine = string.Join(" ", text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
 1119        return singleLine.Length > 120 ? singleLine[..117] + "..." : singleLine;
 120    }
 121
 110122    private Uri BuildUri(string path) => new(_config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/'));
 123}