< Summary

Information
Class: Services.ApiServices.MotexApi.MotexHttpClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MotexApi/MotexHttpClient.cs
Line coverage
88%
Covered lines: 72
Uncovered lines: 9
Coverable lines: 81
Total lines: 147
Line coverage: 88.8%
Branch coverage
64%
Covered branches: 40
Total branches: 62
Branch coverage: 64.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetAsync(...)100%11100%
PostAsync(...)100%11100%
SendAsync()50%1212100%
UnwrapEnvelope(...)56.25%181681.25%
SendAuthorizedAsync()100%22100%
GetTokenAsync()70%393078.57%
BuildUri(...)100%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MotexApi/MotexHttpClient.cs

#LineLine coverage
 1using System.Net;
 2using System.Net.Http.Headers;
 3using Newtonsoft.Json.Linq;
 4using Services.ApiServices.ApiSupplierRequestLog;
 5
 6namespace Services.ApiServices.MotexApi;
 7
 8/// <summary>Bearer-клиент Motex (МоТехС). Токен существует только в памяти текущего процесса.</summary>
 9public sealed class MotexHttpClient
 10{
 11    private readonly IHttpClientFactory _factory;
 12    private readonly MotexConfig _config;
 5413    private readonly SemaphoreSlim _tokenLock = new(1, 1);
 14    private string? _accessToken;
 5415    private DateTimeOffset _expiresAt = DateTimeOffset.MinValue;
 16    private readonly ApiSupplierRequestLogStore? _logs;
 17
 5418    public MotexHttpClient(IHttpClientFactory factory, MotexConfig config, ApiSupplierRequestLogStore? logs = null)
 19    {
 5420        _factory = factory;
 5421        _config = config;
 5422        _logs = logs;
 5423    }
 24
 25    public Task<JToken> GetAsync(string path, IReadOnlyDictionary<string, string?>? query, CancellationToken ct = defaul
 3426        => SendAsync(HttpMethod.Get, path, query, null, ct);
 27
 28    public Task<JToken> PostAsync(string path, JToken body, CancellationToken ct = default)
 1229        => SendAsync(HttpMethod.Post, path, null, body, ct);
 30
 31    private async Task<JToken> SendAsync(HttpMethod method, string path, IReadOnlyDictionary<string, string?>? query, JT
 32    {
 4633        var timer = System.Diagnostics.Stopwatch.StartNew();
 34        HttpResponseMessage response;
 35        try
 36        {
 4637            response = await SendAuthorizedAsync(method, path, query, body, false, ct);
 4338            if (response.StatusCode == HttpStatusCode.Unauthorized)
 39            {
 140                _accessToken = null;
 141                _expiresAt = DateTimeOffset.MinValue;
 142                response.Dispose();
 143                response = await SendAuthorizedAsync(method, path, query, body, true, ct);
 44            }
 4345        }
 346        catch (Exception ex)
 47        {
 348            _logs?.Add("Motex", method.Method, path, null, timer, body?.ToString(), ex.Message);
 349            throw;
 50        }
 51
 4352        using (response)
 53        {
 4354            var text = await response.Content.ReadAsStringAsync(ct);
 4355            _logs?.Add("Motex", method.Method, path, (int)response.StatusCode, timer, body?.ToString(), text);
 4356            if (!response.IsSuccessStatusCode)
 157                throw new MotexApiException($"МоТехС вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode,
 58
 4259            return UnwrapEnvelope(text, path);
 60        }
 3561    }
 62
 63    /// <summary>Motex оборачивает все ответы в {status, messages, response}; отдаёт наружу только response.</summary>
 64    private static JToken UnwrapEnvelope(string text, string path)
 65    {
 66        JObject envelope;
 13667        try { envelope = JObject.Parse(text); }
 068        catch (Exception ex) { throw new MotexApiException("МоТехС вернул невалидный JSON.", requestPath: path, innerExc
 69
 6870        var status = envelope.Value<int?>("status") ?? 200;
 6871        if (status != 200)
 72        {
 773            var messages = envelope["messages"];
 774            var messageText = messages switch
 775            {
 276                JObject obj => obj.Value<string>("errorText") is { Length: > 0 } errorText ? errorText : obj.ToString(Ne
 1077                JArray array => string.Join("; ", array.Select(x => x.ToString())),
 078                null => null,
 079                _ => messages.ToString()
 780            };
 781            throw new MotexApiException(
 782                string.IsNullOrWhiteSpace(messageText) ? $"МоТехС вернул статус {status}." : messageText,
 783                status, path);
 84        }
 85
 6186        return envelope["response"] ?? new JObject();
 87    }
 88
 89    private async Task<HttpResponseMessage> SendAuthorizedAsync(HttpMethod method, string path, IReadOnlyDictionary<stri
 90    {
 4791        var token = await GetTokenAsync(forceRefresh, ct);
 4792        var request = new HttpRequestMessage(method, BuildUri(path, query));
 4793        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
 5994        if (body != null) request.Content = new StringContent(body.ToString(), System.Text.Encoding.UTF8, "application/j
 4795        return await _factory.CreateClient("MotexApi").SendAsync(request, ct);
 4496    }
 97
 98    private async Task<string> GetTokenAsync(bool forceRefresh, CancellationToken ct)
 99    {
 68100        if (!forceRefresh && !string.IsNullOrWhiteSpace(_accessToken) && _expiresAt > DateTimeOffset.UtcNow.AddMinutes(1
 26101        await _tokenLock.WaitAsync(ct);
 102        try
 103        {
 26104            if (!forceRefresh && !string.IsNullOrWhiteSpace(_accessToken) && _expiresAt > DateTimeOffset.UtcNow.AddMinut
 26105            if (string.IsNullOrWhiteSpace(_config.Login) || string.IsNullOrWhiteSpace(_config.Password))
 0106                throw new MotexApiException("Не настроены Motex:Login и/или Motex:Password.");
 107
 26108            var body = new JObject { ["login"] = _config.Login, ["password"] = _config.Password };
 26109            using var content = new StringContent(body.ToString(), System.Text.Encoding.UTF8, "application/json");
 26110            var timer = System.Diagnostics.Stopwatch.StartNew();
 111            HttpResponseMessage response;
 112            string text;
 113            try
 114            {
 26115                response = await _factory.CreateClient("MotexApi").PostAsync(BuildUri("/api/v1/Auth/login", null), conte
 26116                text = await response.Content.ReadAsStringAsync(ct);
 26117            }
 0118            catch (Exception ex)
 119            {
 0120                _logs?.Add("Motex", HttpMethod.Post.Method, "/api/v1/Auth/login", null, timer, body.ToString(), ex.Messa
 0121                throw;
 122            }
 26123            using (response)
 26124                _logs?.Add("Motex", HttpMethod.Post.Method, "/api/v1/Auth/login", (int)response.StatusCode, timer, body.
 26125            if (!response.IsSuccessStatusCode) throw new MotexApiException($"Авторизация МоТехС завершилась HTTP {(int)r
 126
 26127            var result = UnwrapEnvelope(text, "/api/v1/Auth/login");
 26128            var token = result.Value<string>("token");
 26129            var expiresIn = result.Value<int?>("expireIn") ?? 3600;
 26130            if (string.IsNullOrWhiteSpace(token)) throw new MotexApiException("Ответ авторизации МоТехС не содержит toke
 26131            _accessToken = token;
 26132            _expiresAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(60, expiresIn));
 26133            return token;
 134        }
 0135        catch (MotexApiException) { throw; }
 0136        catch (Exception ex) { throw new MotexApiException("Не удалось авторизоваться в МоТехС.", requestPath: "/api/v1/
 26137        finally { _tokenLock.Release(); }
 47138    }
 139
 140    private Uri BuildUri(string path, IReadOnlyDictionary<string, string?>? query)
 141    {
 73142        var builder = new UriBuilder(_config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/'));
 73143        if (query != null)
 69144            builder.Query = string.Join("&", query.Where(x => !string.IsNullOrWhiteSpace(x.Value)).Select(x => $"{Uri.Es
 73145        return builder.Uri;
 146    }
 147}