< Summary

Information
Class: Services.ApiServices.NikeiApi.NikeiHttpClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/NikeiApi/NikeiHttpClient.cs
Line coverage
77%
Covered lines: 45
Uncovered lines: 13
Coverable lines: 58
Total lines: 116
Line coverage: 77.5%
Branch coverage
53%
Covered branches: 16
Total branches: 30
Branch coverage: 53.3%
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()40%131068.18%
PostFormAsync()50%111079.16%
ThrowIfError(...)75%88100%
BuildUri(...)50%2266.66%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/NikeiApi/NikeiHttpClient.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Net.Http.Headers;
 3using System.Text;
 4using Microsoft.Extensions.Options;
 5using Newtonsoft.Json.Linq;
 6using Services.ApiServices.ApiSupplierRequestLog;
 7
 8namespace Services.ApiServices.NikeiApi;
 9
 10/// <summary>Транспорт Nikei: Basic Auth остаётся только в заголовке и никогда не попадает в аудит.</summary>
 11public sealed class NikeiHttpClient
 12{
 13    private const string Provider = "Nikei";
 14
 15    private readonly IHttpClientFactory _factory;
 16    private readonly IOptionsMonitor<NikeiConfig> _options;
 17    private readonly ApiSupplierRequestLogStore? _logs;
 18
 5819    public NikeiHttpClient(IHttpClientFactory factory, IOptionsMonitor<NikeiConfig> options,
 5820        ApiSupplierRequestLogStore? logs = null)
 21    {
 5822        _factory = factory;
 5823        _options = options;
 5824        _logs = logs;
 5825    }
 26
 27    public async Task<JToken> GetAsync(string path, CancellationToken cancellationToken = default)
 28    {
 429        var config = _options.CurrentValue;
 430        if (string.IsNullOrWhiteSpace(config.Login) || string.IsNullOrWhiteSpace(config.Password))
 031            throw new NikeiApiException("Не настроены Nikei:Login и Nikei:Password.", requestPath: path);
 32
 433        var endpoint = "/" + path.TrimStart('/');
 434        var timer = Stopwatch.StartNew();
 35        HttpResponseMessage response;
 36        try
 37        {
 438            using var request = new HttpRequestMessage(HttpMethod.Get, BuildUri(config.BaseUrl, endpoint));
 439            var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{config.Login}:{config.Password}"));
 440            request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
 441            response = await _factory.CreateClient("NikeiApi").SendAsync(request, cancellationToken);
 442        }
 043        catch (Exception ex) when (ex is not NikeiApiException)
 44        {
 045            _logs?.Add(Provider, HttpMethod.Get.Method, endpoint, null, timer, null, ex.Message);
 046            throw new NikeiApiException("Nikei недоступен.", requestPath: endpoint, innerException: ex);
 47        }
 48
 449        using (response)
 50        {
 451            var text = await response.Content.ReadAsStringAsync(cancellationToken);
 452            _logs?.Add(Provider, HttpMethod.Get.Method, endpoint, (int)response.StatusCode, timer, null, text);
 453            if (!response.IsSuccessStatusCode)
 054                throw new NikeiApiException($"Nikei вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, 
 55
 456            try { return JToken.Parse(text); }
 057            catch (Exception ex)
 58            {
 059                throw new NikeiApiException("Nikei вернул невалидный JSON.", (int)response.StatusCode, endpoint, ex);
 60            }
 61        }
 462    }
 63
 64    /// <summary>POST формы Nikei. Токены <c>to_cart</c> передаются в теле, а Basic Auth остаётся в заголовке.</summary>
 65    public async Task<JToken> PostFormAsync(string path, IReadOnlyDictionary<string, string> values,
 66        CancellationToken cancellationToken = default)
 67    {
 2968        var config = _options.CurrentValue;
 2969        if (string.IsNullOrWhiteSpace(config.Login) || string.IsNullOrWhiteSpace(config.Password))
 070            throw new NikeiApiException("Не настроены Nikei:Login и Nikei:Password.", requestPath: path);
 71
 2972        var endpoint = "/" + path.TrimStart('/');
 2973        var timer = Stopwatch.StartNew();
 74        HttpResponseMessage response;
 75        try
 76        {
 2977            using var request = new HttpRequestMessage(HttpMethod.Post, BuildUri(config.BaseUrl, endpoint))
 2978            {
 2979                Content = new FormUrlEncodedContent(values)
 2980            };
 2981            var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{config.Login}:{config.Password}"));
 2982            request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
 2983            response = await _factory.CreateClient("NikeiApi").SendAsync(request, cancellationToken);
 2984        }
 085        catch (Exception ex) when (ex is not NikeiApiException)
 86        {
 087            _logs?.Add(Provider, HttpMethod.Post.Method, endpoint, null, timer, null, ex.Message);
 088            throw new NikeiApiException("Nikei недоступен.", requestPath: endpoint, innerException: ex);
 89        }
 90
 2991        using (response)
 92        {
 2993            var text = await response.Content.ReadAsStringAsync(cancellationToken);
 2994            _logs?.Add(Provider, HttpMethod.Post.Method, endpoint, (int)response.StatusCode, timer, null, text);
 2995            if (!response.IsSuccessStatusCode)
 296                throw new NikeiApiException($"Nikei вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, 
 2797            try { return JToken.Parse(text); }
 098            catch (Exception ex) { throw new NikeiApiException("Nikei вернул невалидный JSON.", (int)response.StatusCode
 99        }
 27100    }
 101
 102    /// <summary>Проверяет бизнес-ошибку, которую Nikei может вернуть с HTTP 200.</summary>
 103    public static void ThrowIfError(JToken token, string path)
 104    {
 15105        if (token is not JObject payload || !string.Equals(payload.Value<string>("status"), "error", StringComparison.Or
 1106        var message = payload.Value<string>("message") ?? payload.Value<string>("text") ?? "Запрос отклонён.";
 1107        throw new NikeiApiException($"Nikei отклонил запрос. {message}", requestPath: path);
 108    }
 109
 110    private static Uri BuildUri(string baseUrl, string path)
 111    {
 33112        if (!Uri.TryCreate(baseUrl.TrimEnd('/') + path, UriKind.Absolute, out var uri))
 0113            throw new NikeiApiException("Некорректный Nikei:BaseUrl.", requestPath: path);
 33114        return uri;
 115    }
 116}