-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathIntegrationTest.cs
More file actions
137 lines (109 loc) · 5.94 KB
/
IntegrationTest.cs
File metadata and controls
137 lines (109 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using FluentAssertions.Extensions;
using JsonApiDotNetCore.Middleware;
using Xunit;
namespace TestBuildingBlocks;
/// <summary>
/// A base class for tests that conveniently enables to execute HTTP requests against JSON:API endpoints. It throttles tests that are running in parallel
/// to avoid exceeding the maximum active database connections.
/// </summary>
public abstract class IntegrationTest : IAsyncLifetime
{
private static readonly MediaTypeHeaderValue DefaultMediaType = MediaTypeHeaderValue.Parse(JsonApiMediaType.Default.ToString());
private static readonly MediaTypeWithQualityHeaderValue OperationsMediaType =
MediaTypeWithQualityHeaderValue.Parse(JsonApiMediaType.AtomicOperations.ToString());
private static readonly SemaphoreSlim ThrottleSemaphore = GetDefaultThrottleSemaphore();
internal static DateTimeOffset DefaultDateTimeUtc { get; } = 1.January(2020).At(1, 2, 3).AsUtc();
protected abstract JsonSerializerOptions SerializerOptions { get; }
private static SemaphoreSlim GetDefaultThrottleSemaphore()
{
int maxConcurrentTestRuns = OperatingSystem.IsWindows() && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSAPPIDDIR")) ? 32 : 64;
return new SemaphoreSlim(maxConcurrentTestRuns);
}
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteGetAsync<TResponseDocument>(string requestUrl,
Action<HttpRequestHeaders>? setRequestHeaders = null)
{
return await ExecuteRequestAsync<TResponseDocument>(HttpMethod.Get, requestUrl, null, null, setRequestHeaders);
}
#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePostAsync<TResponseDocument>(string requestUrl,
object requestBody, string? contentType = null, Action<HttpRequestHeaders>? setRequestHeaders = null)
#pragma warning restore AV1553 // Do not use optional parameters with default value null for strings, collections or tasks
{
MediaTypeHeaderValue mediaType = contentType == null ? DefaultMediaType : MediaTypeHeaderValue.Parse(contentType);
return await ExecuteRequestAsync<TResponseDocument>(HttpMethod.Post, requestUrl, requestBody, mediaType, setRequestHeaders);
}
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePostAtomicAsync<TResponseDocument>(string requestUrl,
object requestBody)
{
Action<HttpRequestHeaders> setRequestHeaders = headers => headers.Accept.Add(OperationsMediaType);
return await ExecuteRequestAsync<TResponseDocument>(HttpMethod.Post, requestUrl, requestBody, OperationsMediaType, setRequestHeaders);
}
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePatchAsync<TResponseDocument>(string requestUrl,
object requestBody, Action<HttpRequestHeaders>? setRequestHeaders = null)
{
return await ExecuteRequestAsync<TResponseDocument>(HttpMethod.Patch, requestUrl, requestBody, DefaultMediaType, setRequestHeaders);
}
public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteDeleteAsync<TResponseDocument>(string requestUrl,
object? requestBody = null, Action<HttpRequestHeaders>? setRequestHeaders = null)
{
return await ExecuteRequestAsync<TResponseDocument>(HttpMethod.Delete, requestUrl, requestBody, DefaultMediaType, setRequestHeaders);
}
private async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteRequestAsync<TResponseDocument>(HttpMethod method,
string requestUrl, object? requestBody, MediaTypeHeaderValue? contentType, Action<HttpRequestHeaders>? setRequestHeaders)
{
using var request = new HttpRequestMessage(method, requestUrl);
string? requestText = SerializeRequest(requestBody);
if (!string.IsNullOrEmpty(requestText))
{
requestText = requestText.Replace("atomic__", "atomic:");
request.Content = new StringContent(requestText);
request.Content.Headers.ContentLength = Encoding.UTF8.GetByteCount(requestText);
if (contentType != null)
{
request.Content.Headers.ContentType = contentType;
}
}
setRequestHeaders?.Invoke(request.Headers);
using HttpClient client = CreateClient();
HttpResponseMessage responseMessage = await client.SendAsync(request);
string responseText = await responseMessage.Content.ReadAsStringAsync();
var responseDocument = DeserializeResponse<TResponseDocument>(responseText);
return (responseMessage, responseDocument!);
}
private string? SerializeRequest(object? requestBody)
{
return requestBody == null ? null : requestBody as string ?? JsonSerializer.Serialize(requestBody, SerializerOptions);
}
protected abstract HttpClient CreateClient();
private TResponseDocument? DeserializeResponse<TResponseDocument>(string responseText)
{
if (typeof(TResponseDocument) == typeof(string))
{
return (TResponseDocument)(object)responseText;
}
if (string.IsNullOrEmpty(responseText))
{
return default;
}
try
{
return JsonSerializer.Deserialize<TResponseDocument>(responseText, SerializerOptions);
}
catch (JsonException exception)
{
throw new FormatException($"Failed to deserialize response body to JSON:\n{responseText}", exception);
}
}
public async Task InitializeAsync()
{
await ThrottleSemaphore.WaitAsync();
}
public virtual Task DisposeAsync()
{
_ = ThrottleSemaphore.Release();
return Task.CompletedTask;
}
}