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

rwjdk / TrelloDotNet / 25865350062

14 May 2026 02:20PM UTC coverage: 81.734% (+3.5%) from 78.2%
25865350062

push

github

rwjdk
More Tests

2721 of 3646 branches covered (74.63%)

Branch coverage included in aggregate %.

4935 of 5721 relevant lines covered (86.26%)

157.9 hits per line

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

86.96
/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 const string NonApiBaseUrl = "https://trello.com/1/";
17
        private readonly HttpClient _httpClient;
18
        private readonly string _apiKey;
19
        private readonly string _token;
20
        private readonly TrelloClient _client;
21

22
        internal HttpClient HttpClient => _httpClient;
3✔
23

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

33
        public string Token => _token;
38✔
34

35
        public string ApiKey => _apiKey;
1✔
36

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

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

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

60
            return responseContent; //Content is assumed JSON
966✔
61
        }
966✔
62

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

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

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

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

97
                return responseContent; //Content is assumed JSON
2✔
98
            }
99
        }
2✔
100

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

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

117
            return content; //Content is assumed JSON
354✔
118
        }
354✔
119

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

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

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

143
            return responseContent; //Content is assumed JSON
150✔
144
        }
150✔
145

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

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

165
            if (response.StatusCode != HttpStatusCode.OK)
40!
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
40✔
171
        }
40✔
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);
145✔
176
            T @object = JsonSerializer.Deserialize<T>(json);
145✔
177
            return @object;
145✔
178
        }
145✔
179

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

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

197
            return responseContent; //Content is assumed JSON
146✔
198
        }
146✔
199

200
        internal async Task<string> PostWithJsonPayloadToNonApi(string suffix, CancellationToken cancellationToken, string payload, int retryCount, params QueryParameter[] parameters)
201
        {
202
            Uri uri = BuildNonApiUri(suffix, parameters);
7✔
203
            HttpResponseMessage response;
204
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri))
7✔
205
            {
206
                request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
7✔
207
                AddCredentialsToHeaderIfNeeded(request);
7✔
208
                response = await _httpClient.SendAsync(request, cancellationToken);
7✔
209
            }
7✔
210
            string responseContent = await response.Content.ReadAsStringAsync();
7✔
211

212
            if (response.StatusCode != HttpStatusCode.OK)
7!
213
            {
214
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PostWithJsonPayload(suffix, cancellationToken, payload, retry, parameters), retryCount, cancellationToken);
×
215
            }
216

217
            return responseContent; //Content is assumed JSON
7✔
218
        }
7✔
219

220
        private string FormatExceptionUrlAccordingToClientOptions(string fullUrl)
221
        {
222
            switch (_client.Options.ApiCallExceptionOption)
7!
223
            {
224
                case ApiCallExceptionOption.IncludeUrlAndCredentials:
225
                    return fullUrl;
1✔
226
                case ApiCallExceptionOption.IncludeUrlButMaskCredentials:
227
                    // ReSharper disable StringLiteralTypo
228
                    return fullUrl.Replace($"key={_apiKey}&token={_token}", "key=XXXXX&token=XXXXXXXXXX");
5✔
229
                case ApiCallExceptionOption.DoNotIncludeTheUrl:
230
                    return string.Empty.PadLeft(5, 'X');
1✔
231
                default:
232
                    throw new ArgumentOutOfRangeException();
×
233
            }
234
        }
235

236
        internal static StringBuilder GetParametersAsString(QueryParameter[] parameters)
237
        {
238
            StringBuilder parameterString = new StringBuilder();
1,792✔
239
            if (parameters == null || parameters.Length == 0)
1,792✔
240
            {
241
                return parameterString;
574✔
242
            }
243

244
            foreach (QueryParameter parameter in parameters)
19,692✔
245
            {
246
                parameterString.Append($"&{parameter.Name}={parameter.GetValueAsApiFormattedString()}");
8,628✔
247
            }
248

249
            return parameterString;
1,218✔
250
        }
251

252
        internal int GetQueryStringLength(params QueryParameter[] parameters)
253
        {
254
            return GetQueryStringCredentialPrefixLength() + GetParametersAsString(parameters).Length;
×
255
        }
256

257
        private Uri BuildUri(string suffix, params QueryParameter[] parameters)
258
        {
259
            string queryString = GetQueryStringCredentials();
1,764✔
260
            string additionalParameters = GetParametersAsString(parameters).ToString();
1,764✔
261
            if (string.IsNullOrWhiteSpace(queryString))
1,764✔
262
            {
263
                additionalParameters = additionalParameters.TrimStart('&');
2✔
264
            }
265

266
            if (string.IsNullOrWhiteSpace(queryString) && string.IsNullOrWhiteSpace(additionalParameters))
1,764✔
267
            {
268
                return new Uri($"{BaseUrl}{suffix}");
1✔
269
            }
270

271
            string separator = suffix.Contains("?") ? "&" : "?";
1,763✔
272
            return new Uri($"{BaseUrl}{suffix}{separator}{queryString}{additionalParameters}");
1,763✔
273
        }
274
        
275
        private Uri BuildNonApiUri(string suffix, params QueryParameter[] parameters)
276
        {
277
            string queryString = GetQueryStringCredentials();
14✔
278
            string additionalParameters = GetParametersAsString(parameters).ToString();
14✔
279
            if (string.IsNullOrWhiteSpace(queryString))
14!
280
            {
281
                additionalParameters = additionalParameters.TrimStart('&');
×
282
            }
283

284
            if (string.IsNullOrWhiteSpace(queryString) && string.IsNullOrWhiteSpace(additionalParameters))
14!
285
            {
286
                return new Uri($"{NonApiBaseUrl}{suffix}");
×
287
            }
288

289
            string separator = suffix.Contains("?") ? "&" : "?";
14!
290
            return new Uri($"{NonApiBaseUrl}{suffix}{separator}{queryString}{additionalParameters}");
14✔
291
        }
292

293

294
        internal async Task<string> Delete(string suffix, CancellationToken cancellationToken, int retryCount)
295
        {
296
            Uri uri = BuildUri(suffix);
100✔
297
            HttpResponseMessage response;
298
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, uri))
100✔
299
            {
300
                AddCredentialsToHeaderIfNeeded(request);
100✔
301
                response = await _httpClient.SendAsync(request, cancellationToken);
100✔
302
            }
100✔
303
            string responseContent = await response.Content.ReadAsStringAsync();
100✔
304

305
            if (response.StatusCode != HttpStatusCode.OK)
100✔
306
            {
307
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Delete(suffix, cancellationToken, retry), retryCount, cancellationToken);
1✔
308
            }
309

310
            return null;
99✔
311
        }
99✔
312

313
        internal async Task<string> DeleteToNonApi(string suffix, CancellationToken cancellationToken, int retryCount)
314
        {
315
            Uri uri = BuildNonApiUri(suffix);
7✔
316
            HttpResponseMessage response;
317
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, uri))
7✔
318
            {
319
                AddCredentialsToHeaderIfNeeded(request);
7✔
320
                response = await _httpClient.SendAsync(request, cancellationToken);
7✔
321
            }
7✔
322
            string responseContent = await response.Content.ReadAsStringAsync();
7✔
323

324
            if (response.StatusCode != HttpStatusCode.OK)
7!
325
            {
326
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Delete(suffix, cancellationToken, retry), retryCount, cancellationToken);
×
327
            }
328

329
            return null;
7✔
330
        }
7✔
331

332
        private async Task<string> PreformRetryIfNeededOrThrow(Uri uri, HttpStatusCode statusCode, string responseContent, Func<int, Task<string>> toRetry, int retryCount, CancellationToken cancellationToken)
333
        {
334
            int statusCodeAsInteger = (int)statusCode;
7✔
335
            bool isTooManyRequestsStatusCode = statusCodeAsInteger == 429;
7✔
336
            if ((responseContent.Contains("API_TOKEN_LIMIT_EXCEEDED") || isTooManyRequestsStatusCode) && retryCount <= _client.Options.MaxRetryCountForTokenLimitExceeded)
7!
337
            {
338
                await Task.Delay(TimeSpan.FromSeconds(_client.Options.DelayInSecondsToWaitInTokenLimitExceededRetry), cancellationToken);
×
339
                retryCount++;
×
340
                return await toRetry(retryCount);
×
341
            }
342

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

346
        private int GetQueryStringCredentialPrefixLength()
347
        {
348
            if (_client.Options.SendCredentialsMode == SendCredentialsMode.Header)
×
349
            {
350
                return 0;
×
351
            }
352

353
            return $"?{GetQueryStringCredentials()}".Length;
×
354
        }
355

356
        private string GetQueryStringCredentials()
357
        {
358
            if (_client.Options.SendCredentialsMode == SendCredentialsMode.Header)
1,778✔
359
            {
360
                return string.Empty;
2✔
361
            }
362

363
            return $"key={_apiKey}&token={_token}";
1,776✔
364
        }
365

366
        private void AddCredentialsToHeaderIfNeeded(HttpRequestMessage request)
367
        {
368
            if (_client.Options.SendCredentialsMode != SendCredentialsMode.Header)
1,778✔
369
            {
370
                return;
1,776✔
371
            }
372

373
            request.Headers.Authorization = AuthenticationHeaderValue.Parse($"OAuth oauth_consumer_key=\"{_apiKey}\", oauth_token=\"{_token}\"");
2✔
374
        }
2✔
375
    }
376
}
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