-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathFakeHttpMessageHandler.cs
More file actions
267 lines (236 loc) · 10.2 KB
/
Copy pathFakeHttpMessageHandler.cs
File metadata and controls
267 lines (236 loc) · 10.2 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.Json;
/// <summary>
/// Useful class for testing HTTP clients.
/// </summary>
public class FakeHttpMessageHandler : HttpMessageHandler
{
readonly List<RequestHandler> _handlers = [];
static HttpResponseMessage CreateResponse<TResponseBody>(TResponseBody responseBody, string contentType) =>
CreateResponse(SerializeObject(responseBody), contentType);
static HttpResponseMessage CreateResponse(string responseBody, string contentType) =>
new()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseBody, Encoding.UTF8, contentType)
};
public RequestHandler AddResponseException(Uri url, HttpMethod httpMethod, Exception responseException)
{
var handler = new RequestHandler(url, httpMethod, responseException);
_handlers.Add(handler);
return handler;
}
public RequestHandler AddResponse(Uri url, HttpMethod httpMethod, HttpResponseMessage responseMessage)
{
var handler = new RequestHandler(url, httpMethod, responseMessage);
_handlers.Add(handler);
return handler;
}
public RequestHandler AddResponse(
Uri url,
HttpMethod httpMethod,
object responseBody,
string contentType = "application/json")
=> AddResponse(url, httpMethod, SerializeObject(responseBody), contentType);
public RequestHandler AddResponse(
Uri url,
HttpMethod httpMethod,
string responseBody,
string contentType = "application/json")
{
#pragma warning disable CA2000
var responseMessage = CreateResponse(responseBody, contentType);
#pragma warning restore CA2000
var handler = new RequestHandler(url, httpMethod, responseMessage);
_handlers.Add(handler);
return handler;
}
public RequestHandler AddResponse<TRequestBody, TResponseBody>(
Uri url,
HttpMethod httpMethod,
Func<TRequestBody, bool> predicate,
TResponseBody responseBody)
{
var handler = RequestHandler.Create(url, httpMethod, predicate, responseBody);
_handlers.Add(handler);
return handler;
}
public void AddRepeatedResponses(
int count,
Uri url,
HttpMethod httpMethod,
Func<int, string> responseBodyFunc,
string contentType = "application/json")
{
for (var i = 0; i < count; i++)
{
AddResponse(url, httpMethod, responseBodyFunc(i), contentType);
}
}
public RequestHandler AddResponse(Uri url, HttpMethod httpMethod, Func<Task<HttpResponseMessage>> responseHandler)
{
var handler = new RequestHandler(url, httpMethod, responseHandler);
_handlers.Add(handler);
return handler;
}
public RequestHandler AddStreamResponse(Func<HttpRequestMessage, Task<bool>> requestPredicate, Stream responseStream)
{
var content = new StreamContent(responseStream);
#pragma warning disable CA2000
var responseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = content
};
#pragma warning restore CA2000
var handler = new RequestHandler(requestPredicate, responseMessage);
_handlers.Add(handler);
return handler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
foreach (var handler in _handlers)
{
if (await handler.IsMatch(request))
{
_handlers.Remove(handler); // Pop the handler so we can simulate multiple requests with different responses.
return await handler.Respond(request);
}
}
return new HttpResponseMessage { StatusCode = HttpStatusCode.NotFound };
}
/// <summary>
/// Handles a request and returns a response.
/// </summary>
/// <param name="requestPredicate">The condition the request must meet to get this response.</param>
/// <param name="responseHandler">
/// A func that returns the response if the <paramref name="requestPredicate"/> is <c>true</c>.
/// </param>
public class RequestHandler(
Func<HttpRequestMessage, Task<bool>> requestPredicate,
Func<Task<HttpResponseMessage>> responseHandler)
{
readonly List<HttpRequestMessage> _receivedRequests = new();
readonly List<string> _receivedBodiesJson = new();
/// <summary>
/// Constructs a <see cref="RequestHandler"/> that throws an exception when the specified url
/// is requested.
/// </summary>
/// <param name="uri">The URL to request.</param>
/// <param name="httpMethod">The HTTP Method.</param>
/// <param name="exception">The exception to throw.</param>
public RequestHandler(Uri uri, HttpMethod httpMethod, Exception exception)
: this(CreateRequestPredicate(uri, httpMethod), () => throw exception)
{
}
/// <summary>
/// Creates a <see cref="RequestHandler"/> that responds with the specified <paramref name="responseMessage"/>
/// when the specified <paramref name="uri"/> is requested with the specified <paramref name="httpMethod"/>.
/// </summary>
/// <param name="uri">The URI to request.</param>
/// <param name="httpMethod">The HTTP method to request with.</param>
/// <param name="responseMessage">The response to respond with.</param>
public RequestHandler(Uri uri, HttpMethod httpMethod, HttpResponseMessage responseMessage)
: this(CreateRequestPredicate(uri, httpMethod), responseMessage)
{
}
/// <summary>
/// Creates a <see cref="RequestHandler"/> that responds response message returned by the specified
/// <paramref name="responseHandler"/> when the specified <paramref name="uri"/> is requested with the
/// specified <paramref name="httpMethod"/>.
/// </summary>
/// <param name="uri">The URI to request.</param>
/// <param name="httpMethod">The HTTP method to request with.</param>
/// <param name="responseHandler">A func that returns a <see cref="HttpResponseMessage"/>.</param>
public RequestHandler(Uri uri, HttpMethod httpMethod, Func<Task<HttpResponseMessage>> responseHandler)
: this(CreateRequestPredicate(uri, httpMethod), responseHandler)
{
}
/// <summary>
/// Creates a <see cref="RequestHandler"/> that responds with the specified <paramref name="responseMessage"/>
/// when the specified <paramref name="requestPredicate"/> is true.
/// </summary>
/// <param name="requestPredicate">The condition the request must meet to get this response.</param>
/// <param name="responseMessage">The response message to return.</param>
public RequestHandler(
Func<HttpRequestMessage, Task<bool>> requestPredicate,
HttpResponseMessage responseMessage)
: this(requestPredicate, () => Task.FromResult(responseMessage))
{
}
public static RequestHandler Create<TRequestBody, TResponseBody>(
Uri uri,
HttpMethod httpMethod,
Func<TRequestBody, bool> requestBodyPredicate,
TResponseBody responseBody,
string contentType = "application/json")
{
return new RequestHandler(
CreateRequestPredicate(uri, httpMethod, requestBodyPredicate),
#pragma warning disable CA2000
CreateResponse(responseBody, contentType));
#pragma warning restore CA2000
}
static Func<HttpRequestMessage, Task<bool>> CreateRequestPredicate(Uri uri, HttpMethod httpMethod)
{
return request => Task.FromResult(request.RequestUri == uri && request.Method == httpMethod);
}
static Func<HttpRequestMessage, Task<bool>> CreateRequestPredicate<TRequestBody>(
Uri uri,
HttpMethod httpMethod,
Func<TRequestBody, bool> requestBodyPredicate)
{
return Predicate;
async Task<bool> Predicate(HttpRequestMessage request) =>
request.RequestUri == uri
&& request.Method == httpMethod
&& request.Content is not null
&& await ReadContentAsync<TRequestBody>(request.Content) is { } requestBody
&& requestBodyPredicate(requestBody);
}
static async Task<T?> ReadContentAsync<T>(HttpContent content)
{
var contentString = await content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(contentString);
}
public Task<bool> IsMatch(HttpRequestMessage requestMessage) => requestPredicate(requestMessage);
public async Task<HttpResponseMessage> Respond(HttpRequestMessage requestMessage)
{
Debug.Assert(requestMessage != null, nameof(requestMessage) + " != null");
// Null-forgiving: Debug.Assert ensures non-null, but ns2.0 lacks [DoesNotReturnIf] annotation
_receivedRequests.Add(requestMessage!);
if (requestMessage!.Content is not null)
{
_receivedBodiesJson.Add(await requestMessage.Content.ReadAsStringAsync());
}
return await responseHandler();
}
public IReadOnlyList<HttpRequestMessage> ReceivedRequests => _receivedRequests;
public HttpRequestMessage ReceivedRequest => _receivedRequests.Single();
public string GetReceivedRequestBody(bool indented)
{
var json = _receivedBodiesJson.Single();
return indented ? FormatJson(json) : json;
}
}
static string FormatJson(string json)
{
using var doc = JsonDocument.Parse(json);
var options = new JsonSerializerOptions
{
WriteIndented = true
};
return JsonSerializer.Serialize(doc.RootElement, options);
}
static string SerializeObject<T>(T obj)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
return JsonSerializer.Serialize(obj, options);
}
}