1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

Rewrite Icon fetching (#3023)

* Rewrite Icon fetching

* Move validation to IconUri, Uri, or UriBuilder

* `dotnet format` 🤖

* PR suggestions

* Add not null compiler hint

* Add twitter to test case

* Move Uri manipulation to UriService

* Implement MockedHttpClient

Presents better, fluent handling of message matching and response
building.

* Add redirect handling tests

* Add testing to models

* More aggressively dispose content in icon link

* Format 🤖

* Update icon lockfile

* Convert to cloned stream for HttpResponseBuilder

Content was being disposed when HttResponseMessage was being disposed.
This avoids losing our reference to our content and allows multiple
usages of the same `MockedHttpMessageResponse`

* Move services to extension

Extension is shared by testing and allows access to services from
our service tests

* Remove unused `using`

* Prefer awaiting asyncs for better exception handling

* `dotnet format` 🤖

* Await async

* Update tests to use test TLD and ip ranges

* Remove unused interfaces

* Make assignments static when possible

* Prefer invariant comparer to downcasing

* Prefer injecting interface services to implementations

* Prefer comparer set in HashSet initialization

* Allow SVG icons

* Filter out icons with unknown formats

* Seek to beginning of MemoryStream after writing it

* More appropriate to not return icon if it's invalid

* Add svg icon test
This commit is contained in:
Matt Gibson
2023-08-08 15:29:40 -04:00
committed by GitHub
parent ca368466ce
commit 4377c7a897
31 changed files with 1685 additions and 522 deletions

View File

@ -0,0 +1,104 @@
#nullable enable
using System.Net;
namespace Bit.Test.Common.MockedHttpClient;
public class HttpRequestMatcher : IHttpRequestMatcher
{
private readonly Func<HttpRequestMessage, bool> _matcher;
private HttpRequestMatcher? _childMatcher;
private MockedHttpResponse _mockedResponse = new(HttpStatusCode.OK);
private bool _responseSpecified = false;
public int NumberOfMatches { get; private set; }
/// <summary>
/// Returns whether or not the provided request can be handled by this matcher chain.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public bool Matches(HttpRequestMessage request) => _matcher(request) && (_childMatcher == null || _childMatcher.Matches(request));
public HttpRequestMatcher(HttpMethod method)
{
_matcher = request => request.Method == method;
}
public HttpRequestMatcher(string uri)
{
_matcher = request => request.RequestUri == new Uri(uri);
}
public HttpRequestMatcher(Uri uri)
{
_matcher = request => request.RequestUri == uri;
}
public HttpRequestMatcher(HttpMethod method, string uri)
{
_matcher = request => request.Method == method && request.RequestUri == new Uri(uri);
}
public HttpRequestMatcher(Func<HttpRequestMessage, bool> matcher)
{
_matcher = matcher;
}
public HttpRequestMatcher WithHeader(string name, string value)
{
return AddChild(request => request.Headers.TryGetValues(name, out var values) && values.Contains(value));
}
public HttpRequestMatcher WithQueryParameters(Dictionary<string, string> requiredQueryParameters) =>
WithQueryParameters(requiredQueryParameters.Select(x => $"{x.Key}={x.Value}").ToArray());
public HttpRequestMatcher WithQueryParameters(string name, string value) =>
WithQueryParameters($"{name}={value}");
public HttpRequestMatcher WithQueryParameters(params string[] queryKeyValues)
{
bool matcher(HttpRequestMessage request)
{
var query = request.RequestUri?.Query;
if (query == null)
{
return false;
}
return queryKeyValues.All(queryKeyValue => query.Contains(queryKeyValue));
}
return AddChild(matcher);
}
/// <summary>
/// Configure how this matcher should respond to matching HttpRequestMessages.
/// Note, after specifying a response, you can no longer further specify match criteria.
/// </summary>
/// <param name="statusCode"></param>
/// <returns></returns>
public MockedHttpResponse RespondWith(HttpStatusCode statusCode)
{
_responseSpecified = true;
_mockedResponse = new MockedHttpResponse(statusCode);
return _mockedResponse;
}
/// <summary>
/// Called to produce an HttpResponseMessage for the given request. This is probably something you want to leave alone
/// </summary>
/// <param name="request"></param>
public async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request)
{
NumberOfMatches++;
return await (_childMatcher == null ? _mockedResponse.RespondToAsync(request) : _childMatcher.RespondToAsync(request));
}
private HttpRequestMatcher AddChild(Func<HttpRequestMessage, bool> matcher)
{
if (_responseSpecified)
{
throw new Exception("Cannot continue to configure a matcher after a response has been specified");
}
_childMatcher = new HttpRequestMatcher(matcher);
return _childMatcher;
}
}

View File

@ -0,0 +1,84 @@
using System.Net;
namespace Bit.Test.Common.MockedHttpClient;
public class HttpResponseBuilder : IDisposable
{
private bool _disposedValue;
public HttpStatusCode StatusCode { get; set; }
public IEnumerable<KeyValuePair<string, string>> Headers { get; set; } = new List<KeyValuePair<string, string>>();
public IEnumerable<string> HeadersToRemove { get; set; } = new List<string>();
public HttpContent Content { get; set; }
public async Task<HttpResponseMessage> ToHttpResponseAsync()
{
var copiedContentStream = new MemoryStream();
await Content.CopyToAsync(copiedContentStream); // This is important, otherwise the content stream will be disposed when the response is disposed.
copiedContentStream.Seek(0, SeekOrigin.Begin);
var message = new HttpResponseMessage(StatusCode)
{
Content = new StreamContent(copiedContentStream),
};
foreach (var header in Headers)
{
message.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return message;
}
public HttpResponseBuilder WithStatusCode(HttpStatusCode statusCode)
{
return new()
{
StatusCode = statusCode,
Headers = Headers,
HeadersToRemove = HeadersToRemove,
Content = Content,
};
}
public HttpResponseBuilder WithHeader(string name, string value)
{
return new()
{
StatusCode = StatusCode,
Headers = Headers.Append(new KeyValuePair<string, string>(name, value)),
HeadersToRemove = HeadersToRemove,
Content = Content,
};
}
public HttpResponseBuilder WithContent(HttpContent content)
{
return new()
{
StatusCode = StatusCode,
Headers = Headers,
HeadersToRemove = HeadersToRemove,
Content = content,
};
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
Content?.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,10 @@
#nullable enable
namespace Bit.Test.Common.MockedHttpClient;
public interface IHttpRequestMatcher
{
int NumberOfMatches { get; }
bool Matches(HttpRequestMessage request);
Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request);
}

View File

@ -0,0 +1,7 @@
namespace Bit.Test.Common.MockedHttpClient;
public interface IMockedHttpResponse
{
int NumberOfResponses { get; }
Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request);
}

View File

@ -0,0 +1,113 @@
#nullable enable
using System.Net;
namespace Bit.Test.Common.MockedHttpClient;
public class MockedHttpMessageHandler : HttpMessageHandler
{
private readonly List<IHttpRequestMatcher> _matchers = new();
/// <summary>
/// The fallback handler to use when the request does not match any of the provided matchers.
/// </summary>
/// <returns>A Matcher that responds with 404 Not Found</returns>
public MockedHttpResponse Fallback { get; set; } = new(HttpStatusCode.NotFound);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var matcher = _matchers.FirstOrDefault(x => x.Matches(request));
if (matcher == null)
{
return await Fallback.RespondToAsync(request);
}
return await matcher.RespondToAsync(request);
}
/// <summary>
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
/// </summary>
/// <param name="requestMatcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T When<T>(T requestMatcher) where T : IHttpRequestMatcher
{
_matchers.Add(requestMatcher);
return requestMatcher;
}
/// <summary>
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
/// </summary>
/// <param name="requestMatcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public HttpRequestMatcher When(string uri)
{
var matcher = new HttpRequestMatcher(uri);
_matchers.Add(matcher);
return matcher;
}
/// <summary>
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
/// </summary>
/// <param name="requestMatcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public HttpRequestMatcher When(Uri uri)
{
var matcher = new HttpRequestMatcher(uri);
_matchers.Add(matcher);
return matcher;
}
/// <summary>
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
/// </summary>
/// <param name="requestMatcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public HttpRequestMatcher When(HttpMethod method)
{
var matcher = new HttpRequestMatcher(method);
_matchers.Add(matcher);
return matcher;
}
/// <summary>
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
/// </summary>
/// <param name="requestMatcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public HttpRequestMatcher When(HttpMethod method, string uri)
{
var matcher = new HttpRequestMatcher(method, uri);
_matchers.Add(matcher);
return matcher;
}
/// <summary>
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
/// </summary>
/// <param name="requestMatcher"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public HttpRequestMatcher When(Func<HttpRequestMessage, bool> matcher)
{
var requestMatcher = new HttpRequestMatcher(matcher);
_matchers.Add(requestMatcher);
return requestMatcher;
}
/// <summary>
/// Converts the MockedHttpMessageHandler to a HttpClient that can be used in your tests after setup.
/// </summary>
/// <returns></returns>
public HttpClient ToHttpClient()
{
return new HttpClient(this);
}
}

View File

@ -0,0 +1,68 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
namespace Bit.Test.Common.MockedHttpClient;
public class MockedHttpResponse : IMockedHttpResponse
{
private MockedHttpResponse _childResponse;
private readonly Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> _responder;
public int NumberOfResponses { get; private set; }
public MockedHttpResponse(HttpStatusCode statusCode)
{
_responder = (_, builder) => builder.WithStatusCode(statusCode);
}
private MockedHttpResponse(Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> responder)
{
_responder = responder;
}
public MockedHttpResponse WithStatusCode(HttpStatusCode statusCode)
{
return AddChild((_, builder) => builder.WithStatusCode(statusCode));
}
public MockedHttpResponse WithHeader(string name, string value)
{
return AddChild((_, builder) => builder.WithHeader(name, value));
}
public MockedHttpResponse WithHeaders(params KeyValuePair<string, string>[] headers)
{
return AddChild((_, builder) => headers.Aggregate(builder, (b, header) => b.WithHeader(header.Key, header.Value)));
}
public MockedHttpResponse WithContent(string mediaType, string content)
{
return WithContent(new StringContent(content, Encoding.UTF8, mediaType));
}
public MockedHttpResponse WithContent(string mediaType, byte[] content)
{
return WithContent(new ByteArrayContent(content) { Headers = { ContentType = new MediaTypeHeaderValue(mediaType) } });
}
public MockedHttpResponse WithContent(HttpContent content)
{
return AddChild((_, builder) => builder.WithContent(content));
}
public async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request)
{
return await RespondToAsync(request, new HttpResponseBuilder());
}
private async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request, HttpResponseBuilder currentBuilder)
{
NumberOfResponses++;
var nextBuilder = _responder(request, currentBuilder);
return await (_childResponse == null ? nextBuilder.ToHttpResponseAsync() : _childResponse.RespondToAsync(request, nextBuilder));
}
private MockedHttpResponse AddChild(Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> responder)
{
_childResponse = new MockedHttpResponse(responder);
return _childResponse;
}
}