Remove HttpClient abstractions

Now implement mocked `HttpMessageHandler`s as recommended: https://stackoverflow.com/a/36427274/3976486
This commit is contained in:
Jordan Dominion
2025-07-27 15:34:33 -04:00
parent 6f5a2f6f6b
commit 9e8bdde30c
38 changed files with 255 additions and 340 deletions
+3 -3
View File
@@ -69,9 +69,9 @@ namespace Tgstation.Server.Client
};
/// <summary>
/// The <see cref="IHttpClient"/> for the <see cref="ApiClient"/>.
/// The <see cref="HttpClient"/> for the <see cref="ApiClient"/>.
/// </summary>
readonly IHttpClient httpClient;
readonly HttpClient httpClient;
/// <summary>
/// The <see cref="IRequestLogger"/>s used by the <see cref="ApiClient"/>.
@@ -166,7 +166,7 @@ namespace Tgstation.Server.Client
/// <param name="tokenRefreshHeaders">The value of <see cref="tokenRefreshHeaders"/>.</param>
/// <param name="authless">The value of <see cref="authless"/>.</param>
public ApiClient(
IHttpClient httpClient,
HttpClient httpClient,
Uri url,
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
@@ -1,7 +1,7 @@
using System;
using System.Net.Http;
using Tgstation.Server.Api;
using Tgstation.Server.Common.Http;
namespace Tgstation.Server.Client
{
@@ -19,5 +19,19 @@ namespace Tgstation.Server.Client
apiHeaders,
tokenRefreshHeaders,
authless);
/// <inheritdoc />
public IApiClient CreateApiClient(
Uri url,
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
HttpMessageHandler handler,
bool disposeHandler,
bool authless) => new ApiClient(
new HttpClient(handler, disposeHandler),
url,
apiHeaders,
tokenRefreshHeaders,
authless);
}
}
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using Tgstation.Server.Api;
@@ -22,5 +23,23 @@ namespace Tgstation.Server.Client
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
bool authless);
/// <summary>
/// Create an <see cref="IApiClient"/>.
/// </summary>
/// <param name="url">The base <see cref="Uri"/>.</param>
/// <param name="apiHeaders">The <see cref="ApiHeaders"/> for the <see cref="IApiClient"/>.</param>
/// <param name="tokenRefreshHeaders">The <see cref="ApiHeaders"/> to use to generate a new <see cref="Api.Models.Response.TokenResponse"/>.</param>
/// <param name="handler">The <see cref="HttpMessageHandler"/> to use with the internal <see cref="HttpClient"/>.</param>
/// <param name="disposeHandler">If <paramref name="handler"/> should be disposed with the created <see cref="IApiClient"/>.</param>
/// <param name="authless">If there should be no authentication performed.</param>
/// <returns>A new <see cref="IApiClient"/>.</returns>
public IApiClient CreateApiClient(
Uri url,
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
HttpMessageHandler handler,
bool disposeHandler,
bool authless);
}
}
@@ -36,7 +36,7 @@ namespace Tgstation.Server.Common.Http
response.Content = null;
try
{
// don't cry about the missing CancellationToken overload: https://github.com/dotnet/runtime/issues/916
// don't cry about the missing CancellationToken overload: https://github.com/dotnet/corefx/issues/32615#issuecomment-562083237
var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
return new CachedResponseStream(content, responseStream);
}
@@ -1,51 +0,0 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Common.Http
{
/// <inheritdoc />
public sealed class HttpClient : IHttpClient
{
/// <inheritdoc />
public TimeSpan Timeout
{
get => httpClient.Timeout;
set => httpClient.Timeout = value;
}
/// <inheritdoc />
public HttpRequestHeaders DefaultRequestHeaders => httpClient.DefaultRequestHeaders;
/// <summary>
/// The real <see cref="System.Net.Http.HttpClient"/>.
/// </summary>
readonly System.Net.Http.HttpClient httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="HttpClient"/> class.
/// </summary>
/// <param name="implementation">The <see cref="System.Net.Http.HttpClient"/> to wrap.</param>
public HttpClient(System.Net.Http.HttpClient implementation)
{
httpClient = implementation ?? throw new ArgumentNullException(nameof(implementation));
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpClient"/> class.
/// </summary>
public HttpClient()
: this(new System.Net.Http.HttpClient())
{
}
/// <inheritdoc />
public void Dispose() => httpClient.Dispose();
/// <inheritdoc />
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
=> httpClient.SendAsync(request, completionOption, cancellationToken);
}
}
@@ -1,41 +0,0 @@
using System;
using System.Net.Http.Headers;
namespace Tgstation.Server.Common.Http
{
/// <summary>
/// <see cref="IAbstractHttpClientFactory"/> that creates <see cref="HttpClient"/>s.
/// </summary>
public sealed class HttpClientFactory : IAbstractHttpClientFactory
{
/// <inheritdoc />
public IHttpClient CreateClient()
{
var client = new HttpClient();
try
{
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
return client;
}
catch
{
client.Dispose();
throw;
}
}
/// <summary>
/// The <see cref="ProductInfoHeaderValue"/> used as created client's User-Agent header on request.
/// </summary>
readonly ProductInfoHeaderValue userAgent;
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientFactory"/> class.
/// </summary>
/// <param name="userAgent">The value of <see cref="userAgent"/>.</param>
public HttpClientFactory(ProductInfoHeaderValue userAgent)
{
this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent));
}
}
}
@@ -1,14 +0,0 @@
namespace Tgstation.Server.Common.Http
{
/// <summary>
/// Creates <see cref="IHttpClient"/>s.
/// </summary>
public interface IAbstractHttpClientFactory
{
/// <summary>
/// Create a <see cref="IHttpClient"/>.
/// </summary>
/// <returns>A new <see cref="IHttpClient"/>.</returns>
IHttpClient CreateClient();
}
}
@@ -1,33 +0,0 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Common.Http
{
/// <summary>
/// For sending HTTP requests.
/// </summary>
public interface IHttpClient : IDisposable
{
/// <summary>
/// The request timeout.
/// </summary>
TimeSpan Timeout { get; set; }
/// <summary>
/// The <see cref="HttpRequestHeaders"/> used on every request.
/// </summary>
HttpRequestHeaders DefaultRequestHeaders { get; }
/// <summary>
/// Send an HTTP request.
/// </summary>
/// <param name="request">The <see cref="HttpRequestMessage"/>.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="HttpResponseMessage"/> of the request.</returns>
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);
}
}
@@ -12,7 +12,6 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.IO;
@@ -56,9 +55,9 @@ namespace Tgstation.Server.Host.Components.Engine
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IAbstractHttpClientFactory"/> for the <see cref="OpenDreamInstallation"/>.
/// The <see cref="IHttpClientFactory"/> for the <see cref="OpenDreamInstallation"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
readonly IHttpClientFactory httpClientFactory;
/// <summary>
/// Path to the Robust.Server.dll.
@@ -84,7 +83,7 @@ namespace Tgstation.Server.Host.Components.Engine
public OpenDreamInstallation(
IIOManager installationIOManager,
IAsyncDelayer asyncDelayer,
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
string dotnetPath,
string serverDllPath,
string compilerDllPath,
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -8,7 +9,6 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
@@ -77,9 +77,9 @@ namespace Tgstation.Server.Host.Components.Engine
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IAbstractHttpClientFactory"/> for the <see cref="OpenDreamInstaller"/>.
/// The <see cref="IHttpClientFactory"/> for the <see cref="OpenDreamInstaller"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
readonly IHttpClientFactory httpClientFactory;
/// <summary>
/// Initializes a new instance of the <see cref="OpenDreamInstaller"/> class.
@@ -100,7 +100,7 @@ namespace Tgstation.Server.Host.Components.Engine
IProcessExecutor processExecutor,
IRepositoryManager repositoryManager,
IAsyncDelayer asyncDelayer,
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions)
: base(ioManager, logger)
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -7,7 +8,6 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
@@ -36,7 +36,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="processExecutor">The <see cref="IProcessExecutor"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="repositoryManager">The <see cref="IRepositoryManager"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> of <see cref="GeneralConfiguration"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> of <see cref="SessionConfiguration"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="linkFactory">The value of <see cref="linkFactory"/>.</param>
@@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Components.Engine
IProcessExecutor processExecutor,
IRepositoryManager repositoryManager,
IAsyncDelayer asyncDelayer,
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions,
IFilesystemLinkFactory linkFactory)
@@ -46,7 +46,6 @@ using Serilog.Sinks.Elasticsearch;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Hubs;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Authority;
using Tgstation.Server.Host.Authority.Core;
using Tgstation.Server.Host.Components;
@@ -311,8 +310,12 @@ namespace Tgstation.Server.Host.Core
services.AddCors();
// Enable managed HTTP clients
services.AddHttpClient();
services.AddSingleton<IAbstractHttpClientFactory, AbstractHttpClientFactory>();
services
.AddHttpClient()
.ConfigureHttpClientDefaults(
builder => builder.ConfigureHttpClient(
client => client.DefaultRequestHeaders.UserAgent.Add(
assemblyInformationProvider.ProductInfoHeaderValue)));
// configure metrics
var prometheusPort = postSetupServices.GeneralConfiguration.PrometheusPort;
@@ -5,7 +5,6 @@ using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api;
using Tgstation.Server.Common.Http;
namespace Tgstation.Server.Host.IO
{
@@ -13,9 +12,9 @@ namespace Tgstation.Server.Host.IO
public sealed class FileDownloader : IFileDownloader
{
/// <summary>
/// The <see cref="IAbstractHttpClientFactory"/> for the <see cref="FileDownloader"/>.
/// The <see cref="IHttpClientFactory"/> for the <see cref="FileDownloader"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
readonly IHttpClientFactory httpClientFactory;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="FileDownloader"/>.
@@ -27,7 +26,7 @@ namespace Tgstation.Server.Host.IO
/// </summary>
/// <param name="httpClientFactory">The value of <see cref="httpClientFactory"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public FileDownloader(IAbstractHttpClientFactory httpClientFactory, ILogger<FileDownloader> logger)
public FileDownloader(IHttpClientFactory httpClientFactory, ILogger<FileDownloader> logger)
{
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -14,9 +14,9 @@ namespace Tgstation.Server.Host.IO
sealed class RequestFileStreamProvider : IFileStreamProvider
{
/// <summary>
/// The <see cref="IHttpClient"/> for the <see cref="RequestFileStreamProvider"/>.
/// The <see cref="HttpClient"/> for the <see cref="RequestFileStreamProvider"/>.
/// </summary>
readonly IHttpClient httpClient;
readonly HttpClient httpClient;
/// <summary>
/// The <see cref="IFileDownloader"/> for the <see cref="RequestFileStreamProvider"/>.
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.IO
/// </summary>
/// <param name="httpClient">The value of <see cref="httpClient"/>.</param>
/// <param name="requestMessage">The value of <see cref="requestMessage"/>.</param>
public RequestFileStreamProvider(IHttpClient httpClient, HttpRequestMessage requestMessage)
public RequestFileStreamProvider(HttpClient httpClient, HttpRequestMessage requestMessage)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.requestMessage = requestMessage ?? throw new ArgumentNullException(nameof(requestMessage));
@@ -1,9 +1,9 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Security.OAuth
@@ -25,11 +25,11 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <summary>
/// Initializes a new instance of the <see cref="DiscordOAuthValidator"/> class.
/// </summary>
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="GenericOAuthValidator"/>.</param>
public DiscordOAuthValidator(
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
ILogger<DiscordOAuthValidator> logger,
OAuthConfiguration oAuthConfiguration)
: base(httpClientFactory, logger, oAuthConfiguration)
@@ -13,7 +13,6 @@ using Newtonsoft.Json.Serialization;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
@@ -53,7 +52,7 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <summary>
/// The <see cref="IHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
readonly IHttpClientFactory httpClientFactory;
/// <summary>
/// Gets <see cref="JsonSerializerSettings"/> that should be used.
@@ -74,7 +73,7 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <param name="logger">The value of <see cref="Logger"/>.</param>
/// <param name="oAuthConfiguration">The value of <see cref="OAuthConfiguration"/>.</param>
public GenericOAuthValidator(
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
ILogger<GenericOAuthValidator> logger,
OAuthConfiguration oAuthConfiguration)
{
@@ -178,10 +177,10 @@ namespace Tgstation.Server.Host.Security.OAuth
protected abstract OAuthTokenRequest CreateTokenRequest(string code);
/// <summary>
/// Create a new configured <see cref="IHttpClient"/>.
/// Create a new configured <see cref="HttpClient"/>.
/// </summary>
/// <returns>A new configured <see cref="IHttpClient"/>.</returns>
IHttpClient CreateHttpClient()
/// <returns>A new configured <see cref="HttpClient"/>.</returns>
HttpClient CreateHttpClient()
{
var httpClient = httpClientFactory.CreateClient();
try
@@ -1,9 +1,9 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Security.OAuth
@@ -27,11 +27,11 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <summary>
/// Initializes a new instance of the <see cref="InvisionCommunityOAuthValidator"/> class.
/// </summary>
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="GenericOAuthValidator"/>.</param>
public InvisionCommunityOAuthValidator(
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
ILogger<InvisionCommunityOAuthValidator> logger,
OAuthConfiguration oAuthConfiguration)
: base(httpClientFactory, logger, oAuthConfiguration)
@@ -1,9 +1,9 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Security.OAuth
@@ -32,11 +32,11 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <summary>
/// Initializes a new instance of the <see cref="KeycloakOAuthValidator"/> class.
/// </summary>
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="GenericOAuthValidator"/>.</param>
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="GenericOAuthValidator"/>.</param>
public KeycloakOAuthValidator(
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
ILogger<KeycloakOAuthValidator> logger,
OAuthConfiguration oAuthConfiguration)
: base(httpClientFactory, logger, oAuthConfiguration)
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Utils.GitHub;
@@ -24,12 +24,12 @@ namespace Tgstation.Server.Host.Security.OAuth
/// Initializes a new instance of the <see cref="OAuthProviders"/> class.
/// </summary>
/// <param name="gitHubServiceFactory">The <see cref="IGitHubServiceFactory"/> to use.</param>
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> to use.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> to use.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use.</param>
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SecurityConfiguration"/> to use.</param>
public OAuthProviders(
IGitHubServiceFactory gitHubServiceFactory,
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
ILoggerFactory loggerFactory,
IOptions<SecurityConfiguration> securityConfigurationOptions)
{
@@ -19,7 +19,6 @@ using Newtonsoft.Json;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
@@ -74,9 +73,9 @@ namespace Tgstation.Server.Host.Swarm
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="IAbstractHttpClientFactory"/> for the <see cref="SwarmService"/>.
/// The <see cref="IHttpClientFactory"/> for the <see cref="SwarmService"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
readonly IHttpClientFactory httpClientFactory;
/// <summary>
/// The <see cref="IAsyncDelayer"/> for the <see cref="SwarmService"/>.
@@ -175,7 +174,7 @@ namespace Tgstation.Server.Host.Swarm
IDatabaseContextFactory databaseContextFactory,
IDatabaseSeeder databaseSeeder,
IAssemblyInformationProvider assemblyInformationProvider,
IAbstractHttpClientFactory httpClientFactory,
IHttpClientFactory httpClientFactory,
IAsyncDelayer asyncDelayer,
IServerUpdater serverUpdater,
IFileTransferTicketProvider transferService,
@@ -1,76 +0,0 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Utils
{
/// <inheritdoc />
sealed class AbstractHttpClientFactory : IAbstractHttpClientFactory
{
/// <summary>
/// The real <see cref="IHttpClientFactory"/>.
/// </summary>
readonly IHttpClientFactory httpClientFactory;
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="AbstractHttpClientFactory"/>.
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="AbstractHttpClientFactory"/>.
/// </summary>
readonly ILogger<AbstractHttpClientFactory> logger;
/// <summary>
/// Initializes a new instance of the <see cref="AbstractHttpClientFactory"/> class.
/// </summary>
/// <param name="httpClientFactory">The value of <see cref="httpClientFactory"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public AbstractHttpClientFactory(
IHttpClientFactory httpClientFactory,
IAssemblyInformationProvider assemblyInformationProvider,
ILogger<AbstractHttpClientFactory> logger)
{
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
#pragma warning disable IDE0079
#pragma warning disable CA2000
public IHttpClient CreateClient()
{
logger.LogTrace("Creating client...");
var innerClient = httpClientFactory.CreateClient();
try
{
var client = new Tgstation.Server.Common.Http.HttpClient(innerClient);
innerClient = null;
try
{
client.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue);
return client;
}
catch
{
client.Dispose();
throw;
}
}
catch
{
innerClient?.Dispose();
throw;
}
}
#pragma warning restore CA2000
#pragma warning restore IDE0079
}
}
@@ -15,6 +15,7 @@ using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Common.Tests;
namespace Tgstation.Server.Client.Tests
{
@@ -44,11 +45,10 @@ namespace Tgstation.Server.Client.Tests
Content = new StringContent(sampleJson)
};
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.SendAsync(It.IsNotNull<HttpRequestMessage>(), It.IsAny<HttpCompletionOption>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(response));
var handler = new MockHttpMessageHandler((_, __) => Task.FromResult(response));
var client = new ApiClient(
httpClient.Object,
new HttpClient(handler),
new Uri("http://fake.com"),
new ApiHeaders(
new ProductHeaderValue("fake"),
@@ -84,11 +84,10 @@ namespace Tgstation.Server.Client.Tests
Content = new StringContent(fakeJson)
};
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.SendAsync(It.IsNotNull<HttpRequestMessage>(), It.IsAny<HttpCompletionOption>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(response));
var handler = new MockHttpMessageHandler((_, __) => Task.FromResult(response));
var client = new ApiClient(
httpClient.Object,
new HttpClient(handler),
new Uri("http://fake.com"),
new ApiHeaders(
new ProductHeaderValue("fake"),
@@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
<ProjectReference Include="..\Tgstation.Server.Common.Tests\Tgstation.Server.Common.Tests.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,19 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tgstation.Server.Common.Extensions.Tests
{
/// <summary>
/// Tests for <see cref="VersionExtensions"/>.
/// </summary>
[TestClass]
public sealed class TestVersionExtensions
{
[TestMethod]
public void TestSemver()
{
Assert.AreEqual(new Version(1, 2, 3), new Version(1, 2, 3, 4).Semver());
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Common.Tests
{
/// <summary>
/// Simple mock <see cref="HttpMessageHandler"/>.
/// </summary>
public sealed class MockHttpMessageHandler : HttpMessageHandler
{
readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback)
{
this.callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
=> callback(request, cancellationToken);
}
}
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../build/TestCommon.props" />
<PropertyGroup>
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Tgstation.Server.Common\Tgstation.Server.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,4 +1,5 @@
using System.Threading;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -8,8 +9,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
@@ -76,7 +75,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
Mock.Of<IProcessExecutor>(),
mockRepositoryManager.Object,
Mock.Of<IAsyncDelayer>(),
Mock.Of<IAbstractHttpClientFactory>(),
Mock.Of<IHttpClientFactory>(),
mockGeneralConfigOptions.Object,
mockSessionConfigOptions.Object);
@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@@ -8,7 +10,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Common.Tests;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.IO.Tests
@@ -27,8 +29,8 @@ Please see the following for more context:
public void TestConstructor()
{
Assert.ThrowsException<ArgumentNullException>(() => new FileDownloader(null, null));
Assert.ThrowsException<ArgumentNullException>(() => new FileDownloader(Mock.Of<IAbstractHttpClientFactory>(), null));
_ = new FileDownloader(Mock.Of<IAbstractHttpClientFactory>(), Mock.Of<ILogger<FileDownloader>>());
Assert.ThrowsException<ArgumentNullException>(() => new FileDownloader(Mock.Of<IHttpClientFactory>(), null));
_ = new FileDownloader(Mock.Of<IHttpClientFactory>(), Mock.Of<ILogger<FileDownloader>>());
}
[TestMethod]
@@ -65,11 +67,21 @@ Please see the following for more context:
builder.SetMinimumLevel(LogLevel.Trace);
});
var mockHttpClientFactory = new Mock<IHttpClientFactory>();
var httpClient = new HttpClient(
new MockHttpMessageHandler(
(_, __) => Task.FromResult(
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(ExpectedData),
})));
mockHttpClientFactory.Setup(x => x.CreateClient(String.Empty)).Returns(httpClient);
try
{
return new FileDownloader(
new HttpClientFactory(
new AssemblyInformationProvider().ProductInfoHeaderValue),
mockHttpClientFactory.Object,
loggerFactory.CreateLogger<FileDownloader>());
}
catch
@@ -10,6 +10,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Common.Tests;
using Tgstation.Server.Host.Extensions;
namespace Tgstation.Server.Host.IO.Tests
@@ -21,7 +22,7 @@ namespace Tgstation.Server.Host.IO.Tests
public async Task TestConstruction()
{
Assert.ThrowsException<ArgumentNullException>(() => new RequestFileStreamProvider(null, null));
var mockClient = Mock.Of<IHttpClient>();
var mockClient = new HttpClient();
Assert.ThrowsException<ArgumentNullException>(() => new RequestFileStreamProvider(mockClient, null));
await using var test = new RequestFileStreamProvider(mockClient, new HttpRequestMessage());
}
@@ -31,21 +32,23 @@ namespace Tgstation.Server.Host.IO.Tests
{
var sequence = new byte[] { 1, 2, 3 };
var resultMs = new MemoryStream(sequence);
var mockHttpClient = new Mock<IHttpClient>();
var response = new HttpResponseMessage()
{
Content = new StreamContent(resultMs),
};
var ran = false;
var request = new HttpRequestMessage();
mockHttpClient
.Setup(x => x.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(response))
.Verifiable();
var mockHttpClient = new HttpClient(
new MockHttpMessageHandler(
(_, _) =>
{
ran = true;
return Task.FromResult(response);
}));
await using var downloader = new RequestFileStreamProvider(mockHttpClient.Object, request);
await using var downloader = new RequestFileStreamProvider(mockHttpClient, request);
var download = await downloader.GetResult(default);
@@ -55,8 +58,7 @@ namespace Tgstation.Server.Host.IO.Tests
var resultSequence = buffer.ToArray();
Assert.IsTrue(sequence.SequenceEqual(resultSequence));
mockHttpClient.VerifyAll();
Assert.IsTrue(ran);
}
[TestMethod]
@@ -64,20 +66,23 @@ namespace Tgstation.Server.Host.IO.Tests
{
var sequence = new byte[] { 1, 2, 3 };
var resultMs = new MemoryStream(sequence);
var mockHttpClient = new Mock<IHttpClient>();
var response = new HttpResponseMessage()
{
Content = new StreamContent(resultMs),
};
int ran = 0;
var request = new HttpRequestMessage();
mockHttpClient
.Setup(x => x.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(response))
.Verifiable();
var mockHttpClient = new HttpClient(
new MockHttpMessageHandler(
(_, _) =>
{
++ran;
return Task.FromResult(response);
}));
await using var downloader = new RequestFileStreamProvider(mockHttpClient.Object, request);
await using var downloader = new RequestFileStreamProvider(mockHttpClient, request);
var task1 = downloader.GetResult(default);
var task2 = downloader.GetResult(default);
@@ -93,15 +98,13 @@ namespace Tgstation.Server.Host.IO.Tests
Assert.AreSame(task1Result, await task2);
Assert.AreSame(task1Result, await task3);
mockHttpClient.VerifyAll();
Assert.AreEqual(1, mockHttpClient.Invocations.Count);
Assert.AreEqual(1, ran);
}
[TestMethod]
public async Task TestInterruptedDownload()
{
var resultMs = new MemoryStream();
var mockHttpClient = new Mock<IHttpClient>();
var response = new HttpResponseMessage()
{
@@ -110,17 +113,17 @@ namespace Tgstation.Server.Host.IO.Tests
var tcs = new TaskCompletionSource<HttpResponseMessage>();
var ran = false;
var request = new HttpRequestMessage();
mockHttpClient
.Setup(x => x.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, It.IsAny<CancellationToken>()))
.Returns<HttpRequestMessage, HttpCompletionOption, CancellationToken>((request, option, cancellationToken) =>
{
cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));
return tcs.Task;
})
.Verifiable();
var mockHttpClient = new HttpClient(
new MockHttpMessageHandler(
(_, _) =>
{
ran = true;
return Task.FromResult(response);
}));
await using var downloader = new RequestFileStreamProvider(mockHttpClient.Object, request);
await using var downloader = new RequestFileStreamProvider(mockHttpClient, request);
using var cts1 = new CancellationTokenSource();
var task1 = downloader.GetResult(cts1.Token);
@@ -133,11 +136,11 @@ namespace Tgstation.Server.Host.IO.Tests
cts2.Cancel();
await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => task1.AsTask());
await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => task2.AsTask());
await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => task3.AsTask());
await Assert.ThrowsExceptionAsync<TaskCanceledException>(task1.AsTask);
await Assert.ThrowsExceptionAsync<TaskCanceledException>(task2.AsTask);
await Assert.ThrowsExceptionAsync<TaskCanceledException>(task3.AsTask);
mockHttpClient.VerifyAll();
Assert.IsTrue(ran);
}
}
}
@@ -21,7 +21,7 @@ using Moq;
using Newtonsoft.Json;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Common.Tests;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Controllers;
using Tgstation.Server.Host.Controllers.Results;
@@ -41,12 +41,10 @@ namespace Tgstation.Server.Host.Swarm.Tests
int serverErrorCount;
public SwarmRpcMapper(Func<SwarmService, FileTransferService, SwarmController> createSwarmController, Mock<IHttpClient> clientMock, ILogger logger)
public SwarmRpcMapper(Func<SwarmService, FileTransferService, SwarmController> createSwarmController, ILogger logger, out HttpMessageHandler handlerMock)
{
this.createSwarmController = createSwarmController;
clientMock
.Setup(x => x.SendAsync(It.IsNotNull<HttpRequestMessage>(), It.IsAny<HttpCompletionOption>(), It.IsAny<CancellationToken>()))
.Returns(MapRequest);
handlerMock = new MockHttpMessageHandler(MapRequest);
this.logger = logger;
AsyncRequests = true;
}
@@ -63,7 +61,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
async Task<HttpResponseMessage> MapRequest(
HttpRequestMessage request,
HttpCompletionOption httpCompletionOption,
CancellationToken cancellationToken)
{
var (config, transferService, node) = configToNodes.FirstOrDefault(
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -50,7 +51,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
public bool Shutdown { get; private set; }
readonly Mock<IHttpClient> mockHttpClient;
readonly Mock<IDatabaseContextFactory> mockDBContextFactory;
readonly Mock<IDatabaseSeeder> mockDatabaseSeeder;
readonly ISetup<IDatabaseSeeder, ValueTask> mockDatabaseSeederInitialize;
@@ -121,10 +121,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
.Setup(x => x.UseContextTaskReturn(It.IsNotNull<Func<IDatabaseContext, Task>>()))
.Callback<Func<IDatabaseContext, Task>>((func) => func(mockDatabaseContext));
var mockHttpClientFactory = new Mock<IAbstractHttpClientFactory>();
mockHttpClient = new Mock<IHttpClient>();
mockHttpClientFactory.Setup(x => x.CreateClient()).Returns(mockHttpClient.Object);
var mockAsyncDelayer = new Mock<IAsyncDelayer>();
mockAsyncDelayer.Setup(
x => x.Delay(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>()))
@@ -161,8 +157,8 @@ namespace Tgstation.Server.Host.Swarm.Tests
targetTransfer,
mockOptions.Object,
loggerFactory.CreateLogger<SwarmController>()),
mockHttpClient,
loggerFactory.CreateLogger($"SwarmRpcMapper-{swarmConfiguration.Identifier}"));
loggerFactory.CreateLogger($"SwarmRpcMapper-{swarmConfiguration.Identifier}"),
out var mockMessageHandler);
mockServerUpdater
.Setup(x => x.BeginUpdate(It.IsNotNull<ISwarmService>(), It.IsAny<IFileStreamProvider>(), It.IsNotNull<Version>(), It.IsAny<CancellationToken>()))
@@ -175,6 +171,9 @@ namespace Tgstation.Server.Host.Swarm.Tests
var mockTokenFactory = new MockTokenFactory();
var mockHttpClientFactory = new Mock<IHttpClientFactory>();
mockHttpClientFactory.Setup(x => x.CreateClient(String.Empty)).Returns(() => new HttpClient(mockMessageHandler));
var runCount = 0;
void RecreateControllerAndService()
{
@@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
<ProjectReference Include="..\Tgstation.Server.Common.Tests\Tgstation.Server.Common.Tests.csproj" />
</ItemGroup>
</Project>
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -174,16 +175,26 @@ namespace Tgstation.Server.Tests
public IFileStreamProvider DownloadFile(Uri url, string bearerToken) => new ProviderPackage(logger, url, bearerToken);
static FileDownloader CreateRealDownloader(ILogger logger)
=> new(
new HttpClientFactory(
new AssemblyInformationProvider().ProductInfoHeaderValue),
public static FileDownloader CreateRealDownloader(ILogger logger)
{
var mockHttpClientFactory = new Mock<IHttpClientFactory>();
mockHttpClientFactory.Setup(x => x.CreateClient(String.Empty)).Returns(
() =>
{
var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.Add(new AssemblyInformationProvider().ProductInfoHeaderValue);
return client;
});
return new (
mockHttpClientFactory.Object,
logger != null
? new Logger<FileDownloader>(
TestingUtils.CreateLoggerFactoryForLogger(
logger,
out _))
: Mock.Of<ILogger<FileDownloader>>());
}
static async Task<MemoryStream> CacheFile(ILogger logger, Uri url, string bearerToken, string path, CancellationToken cancellationToken)
{
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -16,7 +17,6 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
using Tgstation.Server.Client.Components;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Components.Engine;
using Tgstation.Server.Host.Components.Events;
@@ -126,7 +126,7 @@ namespace Tgstation.Server.Tests.Live.Instance
Mock.Of<ILogger<RepositoryManager>>(),
genConfig),
Mock.Of<IAsyncDelayer>(),
Mock.Of<IAbstractHttpClientFactory>(),
Mock.Of<IHttpClientFactory>(),
Options.Create(genConfig),
Options.Create(new SessionConfiguration()))
: new PlatformIdentifier().IsWindows
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Tests.Live
sealed class RateLimitRetryingApiClient : ApiClient
{
public RateLimitRetryingApiClient(
IHttpClient httpClient,
HttpClient httpClient,
Uri url,
ApiHeaders apiHeaders,
ApiHeaders tokenRefreshHeaders,
@@ -1,9 +1,8 @@
using System;
using System.Net.Http;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
using Tgstation.Server.Common.Http;
namespace Tgstation.Server.Tests.Live
{
@@ -20,5 +19,19 @@ namespace Tgstation.Server.Tests.Live
apiHeaders,
tokenRefreshHeaders,
authless);
/// <inheritdoc />
public IApiClient CreateApiClient(
Uri url,
ApiHeaders apiHeaders,
ApiHeaders tokenRefreshHeaders,
HttpMessageHandler handler,
bool disposeHandler,
bool authless) => new RateLimitRetryingApiClient(
new HttpClient(handler, disposeHandler),
url,
apiHeaders,
tokenRefreshHeaders,
authless);
}
}
@@ -409,10 +409,7 @@ namespace Tgstation.Server.Tests.Live
await CheckUpdate();
// Second pass, uploaded updates
var downloader = new Host.IO.FileDownloader(
new Common.Http.HttpClientFactory(
new AssemblyInformationProvider().ProductInfoHeaderValue),
Mock.Of<ILogger<Host.IO.FileDownloader>>());
var downloader = CachingFileDownloader.CreateRealDownloader(Mock.Of<ILogger<Host.IO.FileDownloader>>());
var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
if (String.IsNullOrWhiteSpace(gitHubToken))
gitHubToken = null;
@@ -828,10 +825,7 @@ namespace Tgstation.Server.Tests.Live
CheckInfo(node2Info2);
// also test with uploaded updates this time
var downloader = new Host.IO.FileDownloader(
new Common.Http.HttpClientFactory(
new AssemblyInformationProvider().ProductInfoHeaderValue),
Mock.Of<ILogger<Host.IO.FileDownloader>>());
var downloader = CachingFileDownloader.CreateRealDownloader(Mock.Of<ILogger<Host.IO.FileDownloader>>());
var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
if (String.IsNullOrWhiteSpace(gitHubToken))
gitHubToken = null;
+15
View File
@@ -277,6 +277,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nix", "nix", "{5130526C-A55
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Host.Utils.GitLab.GraphQL", "src\Tgstation.Server.Host.Utils.GitLab.GraphQL\Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj", "{BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Common.Tests", "tests\Tgstation.Server.Common.Tests\Tgstation.Server.Common.Tests.csproj", "{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -571,6 +573,18 @@ Global
{BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU
{BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU
{BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Release|Any CPU.Build.0 = Release|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -609,6 +623,7 @@ Global
{7F7FCFDF-271D-45C2-830C-BCCB19C57077} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2}
{EAB84FD0-5514-4254-B188-7D90ACB7284D} = {316141B0-CD21-4769-A013-D53DA9B9EC09}
{5130526C-A553-493B-A9B0-3DB452949886} = {2648A85F-61AE-428E-95E1-66D06C7A3768}
{73F2C6B9-54A6-4433-9D94-07F2AC0FD084} = {316141B0-CD21-4769-A013-D53DA9B9EC09}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79}