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

rwjdk / TrelloDotNet / 22641078342

03 Mar 2026 08:22PM UTC coverage: 80.414% (-0.09%) from 80.5%
22641078342

push

github

rwjdk
v2.3.0

2632 of 3566 branches covered (73.81%)

Branch coverage included in aggregate %.

155 of 170 new or added lines in 21 files covered. (91.18%)

106 existing lines in 5 files now uncovered.

4676 of 5522 relevant lines covered (84.68%)

149.74 hits per line

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

91.67
/src/TrelloDotNet/Control/ApiRequestController.cs
1
using System;
2
using System.Net;
3
using System.Net.Http;
4
using System.Net.Http.Headers;
5
using System.Text;
6
using System.Text.Json;
7
using System.Threading;
8
using System.Threading.Tasks;
9
using TrelloDotNet.Model;
10

11
namespace TrelloDotNet.Control
12
{
13
    internal class ApiRequestController
14
    {
15
        private const string BaseUrl = "https://api.trello.com/1/";
16
        private readonly HttpClient _httpClient;
17
        private readonly string _apiKey;
18
        private readonly string _token;
19
        private readonly TrelloClient _client;
20

21
        internal HttpClient HttpClient => _httpClient;
3✔
22

23
        internal ApiRequestController(HttpClient httpClient, string apiKey, string token, TrelloClient client)
1,297✔
24
        {
25
            _httpClient = httpClient;
1,297✔
26
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
1,297✔
27
            _apiKey = apiKey;
1,297✔
28
            _token = token;
1,297✔
29
            _client = client;
1,297✔
30
        }
1,297✔
31

32
        public string Token => _token;
36✔
33

34
        public string ApiKey => _apiKey;
1✔
35

36
        internal async Task<T> Get<T>(string suffix, CancellationToken cancellationToken, params QueryParameter[] parameters)
37
        {
38
            string json = await Get(suffix, cancellationToken, 0, parameters);
891✔
39
            var @object = JsonSerializer.Deserialize<T>(json);
891✔
40
            return @object;
891✔
41
        }
891✔
42

43
        internal async Task<string> Get(string suffix, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
44
        {
45
            var uri = BuildUri(suffix, parameters);
913✔
46
            HttpResponseMessage response;
47
            using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
913✔
48
            {
49
                AddCredentialsToHeaderIfNeeded(request);
913✔
50
                response = await _httpClient.SendAsync(request, cancellationToken);
913✔
51
            }
913✔
52
            var responseContent = await response.Content.ReadAsStringAsync();
913✔
53

54
            if (response.StatusCode != HttpStatusCode.OK)
913✔
55
            {
56
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Get(suffix, cancellationToken, retry, parameters), retryCount, cancellationToken);
4✔
57
            }
58

59
            return responseContent; //Content is assumed JSON
909✔
60
        }
909✔
61

62
        internal async Task<T> Post<T>(string suffix, CancellationToken cancellationToken, params QueryParameter[] parameters)
63
        {
64
            string json = await Post(suffix, cancellationToken, 0, parameters);
458✔
65
            var @object = JsonSerializer.Deserialize<T>(json);
458✔
66
            return @object;
458✔
67
        }
458✔
68

69
        internal async Task<T> PostWithAttachmentFileUpload<T>(string suffix, AttachmentFileUpload attachmentFile, CancellationToken cancellationToken, params QueryParameter[] parameters)
70
        {
71
            string json = await PostWithAttachmentFileUpload(suffix, attachmentFile, cancellationToken, 0, parameters);
2✔
72
            var @object = JsonSerializer.Deserialize<T>(json);
2✔
73
            return @object;
2✔
74
        }
2✔
75

76
        internal async Task<string> PostWithAttachmentFileUpload(string suffix, AttachmentFileUpload attachmentFile, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
77
        {
78
            var uri = BuildUri(suffix, parameters);
2✔
79
            using (var multipartFormContent = new MultipartFormDataContent())
2✔
80
            {
81
                multipartFormContent.Add(new StreamContent(attachmentFile.Stream), name: "file", fileName: attachmentFile.Filename);
2✔
82
                HttpResponseMessage response;
83
                using (var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = multipartFormContent })
2✔
84
                {
85
                    AddCredentialsToHeaderIfNeeded(request);
2✔
86
                    response = await _httpClient.SendAsync(request, cancellationToken);
2✔
87
                }
2✔
88
                var responseContent = await response.Content.ReadAsStringAsync();
2✔
89

90
                if (response.StatusCode != HttpStatusCode.OK)
2!
91
                {
92
                    return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PostWithAttachmentFileUpload(suffix, attachmentFile, cancellationToken, retry, parameters), retryCount, cancellationToken);
×
93
                }
94

95
                return responseContent; //Content is assumed JSON
2✔
96
            }
97
        }
2✔
98

99
        internal async Task<string> Post(string suffix, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
100
        {
101
            var uri = BuildUri(suffix, parameters);
462✔
102
            HttpResponseMessage response;
103
            using (var request = new HttpRequestMessage(HttpMethod.Post, uri))
462✔
104
            {
105
                AddCredentialsToHeaderIfNeeded(request);
462✔
106
                response = await _httpClient.SendAsync(request, cancellationToken);
462✔
107
            }
462✔
108
            var content = await response.Content.ReadAsStringAsync();
462✔
109

110
            if (response.StatusCode != HttpStatusCode.OK)
462✔
111
            {
112
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, content, retry => Post(suffix, cancellationToken, retry, parameters), retryCount, cancellationToken);
1✔
113
            }
114

115
            return content; //Content is assumed JSON
461✔
116
        }
461✔
117

118
        internal async Task<T> Put<T>(string suffix, CancellationToken cancellationToken, params QueryParameter[] parameters)
119
        {
120
            string json = await Put(suffix, cancellationToken, 0, parameters);
139✔
121
            var @object = JsonSerializer.Deserialize<T>(json);
139✔
122
            return @object;
139✔
123
        }
139✔
124

125
        internal async Task<string> Put(string suffix, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
126
        {
127
            var uri = BuildUri(suffix, parameters);
145✔
128
            HttpResponseMessage response;
129
            using (var request = new HttpRequestMessage(HttpMethod.Put, uri))
145✔
130
            {
131
                AddCredentialsToHeaderIfNeeded(request);
145✔
132
                response = await _httpClient.SendAsync(request, cancellationToken);
145✔
133
            }
145✔
134
            var responseContent = await response.Content.ReadAsStringAsync();
145✔
135

136
            if (response.StatusCode != HttpStatusCode.OK)
145✔
137
            {
138
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Put(suffix, cancellationToken, retry, parameters), retryCount, cancellationToken);
1✔
139
            }
140

141
            return responseContent; //Content is assumed JSON
144✔
142
        }
144✔
143

144
        internal async Task<T> PutWithJsonPayload<T>(string suffix, CancellationToken cancellationToken, string payload, params QueryParameter[] parameters)
145
        {
146
            string json = await PutWithJsonPayload(suffix, cancellationToken, payload, 0, parameters);
14✔
147
            var @object = JsonSerializer.Deserialize<T>(json);
14✔
148
            return @object;
14✔
149
        }
14✔
150

151
        internal async Task<string> PutWithJsonPayload(string suffix, CancellationToken cancellationToken, string payload, int retryCount, params QueryParameter[] parameters)
152
        {
153
            var uri = BuildUri(suffix, parameters);
32✔
154
            HttpResponseMessage response;
155
            using (var request = new HttpRequestMessage(HttpMethod.Put, uri)
32✔
156
            {
32✔
157
                Content = new StringContent(payload, Encoding.UTF8, "application/json")
32✔
158
            })
32✔
159
            {
160
                AddCredentialsToHeaderIfNeeded(request);
32✔
161
                response = await _httpClient.SendAsync(request, cancellationToken);
32✔
162
            }
32✔
163
            var responseContent = await response.Content.ReadAsStringAsync();
32✔
164

165
            if (response.StatusCode != HttpStatusCode.OK)
32!
166
            {
167
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PutWithJsonPayload(suffix, cancellationToken, payload, retry, parameters), retryCount, cancellationToken);
×
168
            }
169

170
            return responseContent; //Content is assumed JSON
32✔
171
        }
32✔
172

173
        internal async Task<T> PostWithJsonPayload<T>(string suffix, CancellationToken cancellationToken, string payload, params QueryParameter[] parameters)
174
        {
175
            string json = await PostWithJsonPayload(suffix, cancellationToken, payload, 0, parameters);
2✔
176
            var @object = JsonSerializer.Deserialize<T>(json);
2✔
177
            return @object;
2✔
178
        }
2✔
179

180
        internal async Task<string> PostWithJsonPayload(string suffix, CancellationToken cancellationToken, string payload, int retryCount, params QueryParameter[] parameters)
181
        {
182
            var uri = BuildUri(suffix, parameters);
2✔
183
            HttpResponseMessage response;
184
            using (var request = new HttpRequestMessage(HttpMethod.Post, uri)
2✔
185
            {
2✔
186
                Content = new StringContent(payload, Encoding.UTF8, "application/json")
2✔
187
            })
2✔
188
            {
189
                AddCredentialsToHeaderIfNeeded(request);
2✔
190
                response = await _httpClient.SendAsync(request, cancellationToken);
2✔
191
            }
2✔
192
            var responseContent = await response.Content.ReadAsStringAsync();
2✔
193

194
            if (response.StatusCode != HttpStatusCode.OK)
2!
195
            {
196
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PostWithJsonPayload(suffix, cancellationToken, payload, retry, parameters), retryCount, cancellationToken);
×
197
            }
198

199
            return responseContent; //Content is assumed JSON
2✔
200
        }
2✔
201

202
        private string FormatExceptionUrlAccordingToClientOptions(string fullUrl)
203
        {
204
            switch (_client.Options.ApiCallExceptionOption)
7!
205
            {
206
                case ApiCallExceptionOption.IncludeUrlAndCredentials:
207
                    return fullUrl;
1✔
208
                case ApiCallExceptionOption.IncludeUrlButMaskCredentials:
209
                    // ReSharper disable StringLiteralTypo
210
                    return fullUrl.Replace($"key={_apiKey}&token={_token}", "key=XXXXX&token=XXXXXXXXXX");
5✔
211
                case ApiCallExceptionOption.DoNotIncludeTheUrl:
212
                    return string.Empty.PadLeft(5, 'X');
1✔
213
                default:
214
                    throw new ArgumentOutOfRangeException();
×
215
            }
216
        }
217

218
        internal static StringBuilder GetParametersAsString(QueryParameter[] parameters)
219
        {
220
            StringBuilder parameterString = new StringBuilder();
1,796✔
221
            if (parameters == null || parameters.Length == 0)
1,796✔
222
            {
223
                return parameterString;
522✔
224
            }
225

226
            foreach (var parameter in parameters)
22,710✔
227
            {
228
                parameterString.Append($"&{parameter.Name}={parameter.GetValueAsApiFormattedString()}");
10,081✔
229
            }
230

231
            return parameterString;
1,274✔
232
        }
233

234
        internal int GetQueryStringLength(params QueryParameter[] parameters)
235
        {
236
            return GetQueryStringCredentialPrefixLength() + GetParametersAsString(parameters).Length;
138✔
237
        }
238

239
        private Uri BuildUri(string suffix, params QueryParameter[] parameters)
240
        {
241
            string queryString = GetQueryStringCredentials();
1,647✔
242
            string additionalParameters = GetParametersAsString(parameters).ToString();
1,647✔
243
            if (string.IsNullOrWhiteSpace(queryString))
1,647✔
244
            {
245
                additionalParameters = additionalParameters.TrimStart('&');
1✔
246
            }
247

248
            if (string.IsNullOrWhiteSpace(queryString) && string.IsNullOrWhiteSpace(additionalParameters))
1,647✔
249
            {
250
                return new Uri($"{BaseUrl}{suffix}");
1✔
251
            }
252

253
            string separator = suffix.Contains("?") ? "&" : "?";
1,646✔
254
            return new Uri($"{BaseUrl}{suffix}{separator}{queryString}{additionalParameters}");
1,646✔
255
        }
256

257
        internal async Task<string> Delete(string suffix, CancellationToken cancellationToken, int retryCount)
258
        {
259
            var uri = BuildUri(suffix);
91✔
260
            HttpResponseMessage response;
261
            using (var request = new HttpRequestMessage(HttpMethod.Delete, uri))
91✔
262
            {
263
                AddCredentialsToHeaderIfNeeded(request);
91✔
264
                response = await _httpClient.SendAsync(request, cancellationToken);
91✔
265
            }
91✔
266
            var responseContent = await response.Content.ReadAsStringAsync();
91✔
267

268
            if (response.StatusCode != HttpStatusCode.OK)
91✔
269
            {
270
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Delete(suffix, cancellationToken, retry), retryCount, cancellationToken);
1✔
271
            }
272

273
            return null;
90✔
274
        }
90✔
275

276
        private async Task<string> PreformRetryIfNeededOrThrow(Uri uri, HttpStatusCode statusCode, string responseContent, Func<int, Task<string>> toRetry, int retryCount, CancellationToken cancellationToken)
277
        {
278
            int statusCodeAsInteger = (int)statusCode;
7✔
279
            bool isTooManyRequestsStatusCode = statusCodeAsInteger == 429;
7✔
280
            if ((responseContent.Contains("API_TOKEN_LIMIT_EXCEEDED") || isTooManyRequestsStatusCode) && retryCount <= _client.Options.MaxRetryCountForTokenLimitExceeded)
7!
281
            {
UNCOV
282
                await Task.Delay(TimeSpan.FromSeconds(_client.Options.DelayInSecondsToWaitInTokenLimitExceededRetry), cancellationToken);
×
UNCOV
283
                retryCount++;
×
UNCOV
284
                return await toRetry(retryCount);
×
285
            }
286

287
            throw new TrelloApiException($"{responseContent} [{statusCodeAsInteger}: {statusCode}]", FormatExceptionUrlAccordingToClientOptions(uri.AbsoluteUri), statusCode); //Content is assumed Error Message       
7✔
UNCOV
288
        }
×
289

290
        private int GetQueryStringCredentialPrefixLength()
291
        {
292
            if (_client.Options.SendCredentialsMode == SendCredentialsMode.Header)
138!
293
            {
NEW
294
                return 0;
×
295
            }
296

297
            return $"?{GetQueryStringCredentials()}".Length;
138✔
298
        }
299

300
        private string GetQueryStringCredentials()
301
        {
302
            if (_client.Options.SendCredentialsMode == SendCredentialsMode.Header)
1,785✔
303
            {
304
                return string.Empty;
1✔
305
            }
306

307
            return $"key={_apiKey}&token={_token}";
1,784✔
308
        }
309

310
        private void AddCredentialsToHeaderIfNeeded(HttpRequestMessage request)
311
        {
312
            if (_client.Options.SendCredentialsMode != SendCredentialsMode.Header)
1,647✔
313
            {
314
                return;
1,646✔
315
            }
316

317
            request.Headers.Authorization = AuthenticationHeaderValue.Parse($"OAuth oauth_consumer_key=\"{_apiKey}\", oauth_token=\"{_token}\"");
1✔
318
        }
1✔
319
    }
320
}
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

© 2026 Coveralls, Inc