• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Shane32 / PostGrid / 13884143274

16 Mar 2025 02:03PM UTC coverage: 25.333% (+6.3%) from 19.013%
13884143274

Pull #2

github

web-flow
Merge de0a5a6d0 into 088e5f792
Pull Request #2: Test all contact methods & fix delete

50 of 266 branches covered (18.8%)

Branch coverage included in aggregate %.

7 of 11 new or added lines in 7 files covered. (63.64%)

2 existing lines in 1 file now uncovered.

197 of 709 relevant lines covered (27.79%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

46.53
/src/Project/PostGridConnection.cs
1
using System.Text.Json;
2
using System.Text.Json.Serialization.Metadata;
3
using Microsoft.Extensions.Options;
4

5
namespace Shane32.PostGrid;
6

7
/// <summary>
8
/// Implementation of <see cref="IPostGridConnection"/> for connecting to the PostGrid API.
9
/// </summary>
10
public partial class PostGridConnection : IPostGridConnection
11
{
12
    private readonly HttpClient _httpClient;
13
    private readonly PostGridOptions _options;
14

15
    /// <summary>
16
    /// Initializes a new instance of the <see cref="PostGridConnection"/> class.
17
    /// </summary>
18
    /// <param name="httpClient">The HTTP client to use for API requests.</param>
19
    /// <param name="options">The options for configuring the PostGrid API client.</param>
20
    public PostGridConnection(HttpClient httpClient, IOptions<PostGridOptions> options)
4✔
21
    {
4✔
22
        _httpClient = httpClient;
4✔
23
        _options = options.Value;
4✔
24
    }
4✔
25

26
    /// <inheritdoc />
27
    public virtual ValueTask<string> GetApiKey(CancellationToken cancellationToken = default)
28
    {
4✔
29
        var apiKey = _options.ApiKey;
4✔
30
        if (apiKey == null || string.IsNullOrWhiteSpace(apiKey)) {
4!
31
            throw new InvalidOperationException("The API key is not configured.");
×
32
        }
33
        return new ValueTask<string>(apiKey);
4✔
34
    }
4✔
35

36
    /// <summary>
37
    /// Sends an HTTP request to the PostGrid API and processes the response.
38
    /// The API key is added to the request headers.
39
    /// </summary>
40
    /// <typeparam name="T">The type to deserialize the response content to.</typeparam>
41
    /// <param name="requestFactory">A function that creates the HTTP request message to send.</param>
42
    /// <param name="deserializeFunc">A function to deserialize the response content stream to type T.</param>
43
    /// <param name="cancellationToken">A token to cancel the operation.</param>
44
    /// <returns>The deserialized response object.</returns>
45
    /// <exception cref="PostGridException">Thrown when the request fails and the error response can be deserialized.</exception>
46
    /// <exception cref="HttpRequestException">Thrown when the request fails and the error response cannot be deserialized.</exception>
47
    protected virtual async Task<T> SendRequestAsync<T>(Func<HttpRequestMessage> requestFactory, Func<Stream, CancellationToken, ValueTask<T>> deserializeFunc, CancellationToken cancellationToken = default)
48
    {
4✔
49
        int retryCount = 0;
4✔
50
        HttpResponseMessage? response = null;
4✔
51

52
        // Get the API key
53
        var apiKey = await GetApiKey(cancellationToken);
4✔
54

55
        while (true) {
8✔
56
            // Create a new request for each attempt
57
            using var request = requestFactory();
4✔
58

59
            // Add the API key header to the request
60
            request.Headers.Add("x-api-key", apiKey);
4✔
61

62
            // Send the request
63
            response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
4✔
64

65
            // If we got a 429 status code (rate limit) and retries are enabled
66
            if ((int)response.StatusCode == 429 &&
4!
67
                _options.MaxRetryAttempts > 0 &&
4✔
68
                retryCount < _options.MaxRetryAttempts) {
4✔
69
                // Increment retry count
70
                retryCount++;
×
71

72
                // Determine if we should retry and how long to wait
73
                TimeSpan? retryDelay;
74

75
                if (_options.ShouldRetryAsync != null) {
×
76
                    // Use the custom retry strategy
77
                    retryDelay = await _options.ShouldRetryAsync(retryCount);
×
78
                } else {
×
79
                    // Use default exponential backoff strategy
80
                    retryDelay = TimeSpan.FromMilliseconds(_options.DefaultRetryDelayMs * Math.Pow(2, retryCount - 1));
×
81
                }
×
82

83
                // If retryDelay is null, stop retrying
84
                if (retryDelay == null) {
×
85
                    break;
×
86
                }
87

88
                // If retryDelay is positive, wait before retrying
89
                if (retryDelay.Value > TimeSpan.Zero) {
×
90
                    await Task.Delay(retryDelay.Value, cancellationToken);
×
91
                }
×
92

93
                // Dispose the current response before retrying
94
                response.Dispose();
×
95

96
                // Continue to the next iteration (retry)
97
                continue;
×
98
            }
99

100
            // Break the loop if we're not retrying
101
            break;
4✔
102
        }
103

104
        // Check for success status code
105
        if (!response.IsSuccessStatusCode) {
4!
106
            // Check if the content type is JSON
107
            var isJson = response.Content.Headers.ContentType?.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) ?? false;
×
108

109
            if (isJson) {
×
110
                try {
×
111
                    // Try to deserialize the error response
NEW
112
                    var errorString = await response.Content.ReadAsStringAsync();
×
NEW
113
                    var errorResponse = JsonSerializer.Deserialize(errorString, PostGridJsonSerializerContext.Default.ErrorResponse);
×
114
                    //var errorStream = await response.Content.ReadAsStreamAsync();
115
                    //var errorResponse = await JsonSerializer.DeserializeAsync(errorStream, PostGridJsonSerializerContext.Default.ErrorResponse, cancellationToken);
116

117
                    if (errorResponse != null && errorResponse.Type != null) {
×
118
                        throw new PostGridException(
×
119
                            errorResponse.Type,
×
120
                            errorResponse.Message ?? "Unknown error",
×
121
                            response.StatusCode,
×
122
                            null);
×
123
                    }
124
                } catch (JsonException) {
×
125
                    // If deserialization fails, we'll fall back to the default HttpRequestException
126
                }
×
127
            }
×
128

129
            // If we couldn't deserialize the error response, throw a standard HttpRequestException
130
            throw new HttpRequestException(
×
131
                $"The request failed with status code {response.StatusCode} and was unable to be parsed.");
×
132
        }
133

134
        // Get the response content as a stream
135
        var contentStream = await response.Content.ReadAsStreamAsync();
4✔
136

137
        // Deserialize the response using the provided function
138
        return await deserializeFunc(contentStream, cancellationToken);
4✔
139
    }
4✔
140

141
    /// <summary>
142
    /// Sends an HTTP request to the PostGrid API and processes the response using JSON source generation.
143
    /// </summary>
144
    /// <typeparam name="T">The type to deserialize the response content to.</typeparam>
145
    /// <param name="requestFactory">A function that creates the HTTP request message to send.</param>
146
    /// <param name="jsonTypeInfo">The JSON type information for deserialization.</param>
147
    /// <param name="cancellationToken">A token to cancel the operation.</param>
148
    /// <returns>The deserialized response object.</returns>
149
    /// <exception cref="PostGridException">Thrown when the request fails and the error response can be deserialized.</exception>
150
    /// <exception cref="HttpRequestException">Thrown when the request fails and the error response cannot be deserialized.</exception>
151
    protected virtual Task<T> SendRequestAsync<T>(Func<HttpRequestMessage> requestFactory, JsonTypeInfo<T> jsonTypeInfo, CancellationToken cancellationToken = default)
152
    {
4✔
153
        return SendRequestAsync(
4✔
154
            requestFactory,
4✔
155
            /*
4✔
156
            async (stream, token) => await JsonSerializer.DeserializeAsync(stream, jsonTypeInfo, token)
4✔
157
                ?? throw new JsonException("The response returned a null object."),
4✔
158
            */
4✔
159
            async (stream, token) => {
4✔
160
                // Deserialize the response to a string for debugging
4✔
161
                var responseString = await new StreamReader(stream).ReadToEndAsync();
4✔
162
                return JsonSerializer.Deserialize<T>(responseString, jsonTypeInfo)
4✔
163
                    ?? throw new JsonException("The response returned a null object.");
4✔
164
            },
4✔
165
            cancellationToken);
4✔
166
    }
4✔
167
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc