< Summary

Information
Class: Services.ApiServices.FavoritPartsApi.FavoritPartsOrderHttpClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/FavoritPartsApi/FavoritPartsOrderHttpClient.cs
Line coverage
84%
Covered lines: 44
Uncovered lines: 8
Coverable lines: 52
Total lines: 105
Line coverage: 84.6%
Branch coverage
56%
Covered branches: 13
Total branches: 23
Branch coverage: 56.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%210%
SendAsync()57.14%242181.08%
Shorten(...)50%22100%
BuildUri(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/FavoritPartsApi/FavoritPartsOrderHttpClient.cs

#LineLine coverage
 1using Microsoft.Extensions.Options;
 2using Newtonsoft.Json.Linq;
 3using Services.ApiServices.ApiSupplierRequestLog;
 4
 5namespace Services.ApiServices.FavoritPartsApi;
 6
 7/// <summary>Клиент REST v1 FavoritParts (ws/v1/) — справочники, реальная серверная корзина, реальные заказы.
 8/// Отдельный от проценки хост-путь. Авторизация — заголовки X-Favorit-ClientKey/X-Favorit-DeveloperKey
 9/// (ключи не попадают в query, поэтому не требуют редактирования в логе запросов).
 10/// Коды ответа: 200 успех (JSON), 401 ошибка авторизации, 400 ошибка сервиса (текст в теле, Content-Type
 11/// text/html — не JSON), 500 ошибка сервера, 403/404 на GET order/{id} — чужой/несуществующий заказ.</summary>
 12public class FavoritPartsOrderHttpClient
 13{
 14    private const string ProviderKey = "FavoritParts";
 15    private readonly IHttpClientFactory _factory;
 16    private readonly IOptionsMonitor<FavoritPartsConfig> _config;
 17    private readonly ApiSupplierRequestLogStore _logs;
 18    private readonly string _providerKey;
 19    private readonly string _optionsName;
 20
 3421    public FavoritPartsOrderHttpClient(IHttpClientFactory factory, IOptionsMonitor<FavoritPartsConfig> config, ApiSuppli
 3422        string providerKey = "FavoritParts", string optionsName = "")
 23    {
 3424        _factory = factory;
 3425        _config = config;
 3426        _logs = logs;
 3427        _providerKey = providerKey;
 3428        _optionsName = optionsName;
 3429    }
 30
 31    /// <summary>query поддерживает повторяющиеся ключи (например orderID[]=..&amp;orderID[]=..) — обычный Dictionary дл
 32    public Task<JToken> GetAsync(string path, IEnumerable<KeyValuePair<string, string?>>? query = null, CancellationToke
 3133        SendAsync(HttpMethod.Get, path, query, body: null, ct);
 34
 35    public Task<JToken> PostAsync(string path, string jsonBody, CancellationToken ct = default) =>
 036        SendAsync(HttpMethod.Post, path, query: null, body: jsonBody, ct);
 37
 38    private async Task<JToken> SendAsync(HttpMethod method, string path, IEnumerable<KeyValuePair<string, string?>>? que
 39    {
 3140        var config = string.IsNullOrEmpty(_optionsName) ? _config.CurrentValue : _config.Get(_optionsName);
 3141        if (string.IsNullOrWhiteSpace(config.ClientKey))
 042            throw new FavoritPartsApiException("Не настроен FavoritParts:ClientKey.");
 3143        if (string.IsNullOrWhiteSpace(config.DeveloperKey))
 044            throw new FavoritPartsApiException("Не настроен FavoritParts:DeveloperKey. Запрос в боевой API без него не о
 45
 3146        var uri = BuildUri(config, path, query ?? Array.Empty<KeyValuePair<string, string?>>());
 3147        var requestLog = $"{uri.Query}\n" +
 3148                         $"Client authentication: present (length={config.ClientKey.Length})\n" +
 3149                         $"Developer authentication: present (length={config.DeveloperKey.Length})";
 50
 3151        var timer = System.Diagnostics.Stopwatch.StartNew();
 52        try
 53        {
 3154            using var request = new HttpRequestMessage(method, uri);
 3155            request.Headers.Add("X-Favorit-ClientKey", config.ClientKey);
 3156            request.Headers.Add("X-Favorit-DeveloperKey", config.DeveloperKey);
 3157            if (body is not null)
 058                request.Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json");
 59
 3160            using var response = await _factory.CreateClient("FavoritPartsOrderApi").SendAsync(request, ct);
 2961            var text = await response.Content.ReadAsStringAsync(ct);
 2962            _logs.Add(_providerKey, method.Method, path, (int)response.StatusCode, timer, requestLog, text);
 63
 2964            if (!response.IsSuccessStatusCode)
 65            {
 66                // Текст ошибки сервиса приходит без какого-либо контекста (вплоть до «Что-то пошло не так»),
 67                // поэтому в сообщение всегда добавляем код и адрес: иначе в UI не отличить сбой поставщика
 68                // от нашей ошибки.
 169                var message = response.StatusCode switch
 170                {
 071                    System.Net.HttpStatusCode.Unauthorized => "FavoritParts: ошибка авторизации (неверный/просроченный C
 072                    System.Net.HttpStatusCode.Forbidden => "FavoritParts: неверный идентификатор заказа.",
 073                    System.Net.HttpStatusCode.NotFound => "FavoritParts: заказ не найден или принадлежит другому покупат
 174                    _ => string.IsNullOrWhiteSpace(text)
 175                        ? $"FavoritParts вернул HTTP {(int)response.StatusCode} на {path}."
 176                        : $"FavoritParts (HTTP {(int)response.StatusCode}, {path}): {Shorten(text.Trim())}"
 177                };
 178                throw new FavoritPartsApiException(message, (int)response.StatusCode, path);
 79            }
 80
 2881            if (string.IsNullOrWhiteSpace(text)) return JValue.CreateNull();
 82
 2883            try { return JToken.Parse(text); }
 084            catch (Exception ex) { throw new FavoritPartsApiException("FavoritParts вернул невалидный JSON.", requestPat
 85        }
 286        catch (FavoritPartsApiException) { throw; }
 287        catch (Exception ex)
 88        {
 289            _logs.Add(_providerKey, method.Method, path, null, timer, requestLog, ex.Message);
 290            throw new FavoritPartsApiException("Не удалось выполнить запрос к FavoritParts.", requestPath: path, innerEx
 91        }
 2892    }
 93
 94    /// <summary>Тело ошибки приходит как text/html и может оказаться целой страницей — в сообщение
 95    /// для пользователя тащим только начало.</summary>
 196    private static string Shorten(string text) => text.Length <= 300 ? text : text[..300] + "…";
 97
 98    private static Uri BuildUri(FavoritPartsConfig config, string path, IEnumerable<KeyValuePair<string, string?>> query
 99    {
 31100        var builder = new UriBuilder(config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/'));
 31101        builder.Query = string.Join("&", query.Where(x => !string.IsNullOrWhiteSpace(x.Value))
 31102            .Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value!)}"));
 31103        return builder.Uri;
 104    }
 105}