< Summary

Information
Class: Services.ApiServices.PartsnavigatorApi.PartsnavigatorApiClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/PartsnavigatorApi/PartsnavigatorApiClient.cs
Line coverage
95%
Covered lines: 42
Uncovered lines: 2
Coverable lines: 44
Total lines: 95
Line coverage: 95.4%
Branch coverage
70%
Covered branches: 24
Total branches: 34
Branch coverage: 70.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
LookupAsync()69.23%2626100%
ParseLookup(...)83.33%6687.5%
TryParseError(...)50%2275%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/PartsnavigatorApi/PartsnavigatorApiClient.cs

#LineLine coverage
 1using System.Net;
 2using System.Text.Json;
 3using Microsoft.Extensions.Logging;
 4
 5namespace Services.ApiServices.PartsnavigatorApi;
 6
 7public interface IPartsnavigatorApiClient
 8{
 9    /// <param name="withAnalogs">false — быстрая карточка (<c>analogs=false</c>), true — вместе со страницей аналогов (
 10    Task<PartsnavigatorResult> LookupAsync(string brand, string number, bool withAnalogs, CancellationToken ct = default
 11}
 12
 13/// <summary>
 14/// HTTP-клиент парсера partsnavigator.com.au. Все сбои (сеть, таймаут, 4xx/5xx, битый JSON) возвращаются
 15/// в <see cref="PartsnavigatorResult"/>, исключения наружу не выходят (кроме отмены вызывающим).
 16/// </summary>
 17public sealed class PartsnavigatorApiClient : IPartsnavigatorApiClient
 18{
 119    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
 20
 21    private readonly IHttpClientFactory _factory;
 22    private readonly PartsnavigatorConfig _config;
 23    private readonly ILogger<PartsnavigatorApiClient> _logger;
 24
 1225    public PartsnavigatorApiClient(IHttpClientFactory factory, PartsnavigatorConfig config, ILogger<PartsnavigatorApiCli
 26    {
 1227        _factory = factory;
 1228        _config = config;
 1229        _logger = logger;
 1230    }
 31
 32    public async Task<PartsnavigatorResult> LookupAsync(string brand, string number, bool withAnalogs, CancellationToken
 33    {
 1334        var path = $"partsnavigator/{Uri.EscapeDataString(brand)}/{Uri.EscapeDataString(number)}?analogs={(withAnalogs ?
 1335        var timeout = TimeSpan.FromSeconds(Math.Max(5, withAnalogs ? _config.AnalogsTimeoutSeconds : _config.CardTimeout
 36        try
 37        {
 1338            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
 1339            timeoutCts.CancelAfter(timeout);
 1340            var client = _factory.CreateClient(PartsnavigatorConfig.HttpClientName);
 1341            using var response = await client.GetAsync(path, timeoutCts.Token);
 942            var body = await response.Content.ReadAsStringAsync(timeoutCts.Token);
 43
 944            if (response.StatusCode == HttpStatusCode.OK)
 545                return ParseLookup(body, brand, number);
 46
 447            var error = TryParseError(body);
 448            if (response.StatusCode == HttpStatusCode.NotFound && !string.Equals(error?.Code, "INTERNAL_ERROR", StringCo
 149                return PartsnavigatorResult.NotFound(error?.Message);
 50
 351            _logger.LogWarning("Partsnavigator {Brand} {Number}: HTTP {Status} {Code}", brand, number, (int)response.Sta
 352            return PartsnavigatorResult.Failure(
 353                error?.Code ?? $"HTTP_{(int)response.StatusCode}",
 354                error?.Message ?? "Сервис Partsnavigator вернул ошибку.",
 355                error?.Retry ?? (int)response.StatusCode >= 500);
 56        }
 357        catch (OperationCanceledException) when (ct.IsCancellationRequested)
 58        {
 159            throw;
 60        }
 261        catch (OperationCanceledException)
 62        {
 263            _logger.LogInformation("Partsnavigator {Brand} {Number}: таймаут запроса", brand, number);
 264            return PartsnavigatorResult.Failure("TIMEOUT", "Сервис Partsnavigator не ответил вовремя.", true);
 65        }
 166        catch (HttpRequestException ex)
 67        {
 168            _logger.LogWarning(ex, "Partsnavigator {Brand} {Number}: сервис недоступен", brand, number);
 169            return PartsnavigatorResult.Failure("UNAVAILABLE", "Сервис Partsnavigator недоступен.", true);
 70        }
 1271    }
 72
 73    private PartsnavigatorResult ParseLookup(string body, string brand, string number)
 74    {
 75        try
 76        {
 577            var lookup = JsonSerializer.Deserialize<PartsnavigatorLookup>(body, JsonOptions);
 478            if (lookup is null)
 079                return PartsnavigatorResult.Failure("BAD_RESPONSE", "Пустой ответ сервиса Partsnavigator.", true);
 480            return lookup.Exists && lookup.Requested is not null ? PartsnavigatorResult.Found(lookup) : PartsnavigatorRe
 81        }
 182        catch (JsonException ex)
 83        {
 184            _logger.LogWarning(ex, "Partsnavigator {Brand} {Number}: не удалось разобрать ответ", brand, number);
 185            return PartsnavigatorResult.Failure("BAD_RESPONSE", "Не удалось разобрать ответ сервиса Partsnavigator.", tr
 86        }
 587    }
 88
 89    private static PartsnavigatorErrorBody? TryParseError(string body)
 90    {
 491        if (string.IsNullOrWhiteSpace(body)) return null;
 492        try { return JsonSerializer.Deserialize<PartsnavigatorErrorBody>(body, JsonOptions); }
 093        catch (JsonException) { return null; }
 494    }
 95}