Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ jobs:
with:
username: postgres
password: postgres
- name: Configure PostgreSQL
shell: bash
run: |
PGDATA="$RUNNER_TEMP/pgdata"
echo "max_connections = 500" >> "$PGDATA/postgresql.conf"
pg_ctl restart --pgdata="$PGDATA" --wait
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
Expand Down Expand Up @@ -97,13 +103,6 @@ jobs:
- name: Build
run: dotnet build --no-restore --configuration Release /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }}
- name: Test
env:
# Override log levels, to reduce logging output when running tests in ci-build.
Logging__LogLevel__Microsoft.Hosting.Lifetime: 'None'
Logging__LogLevel__Microsoft.AspNetCore.Hosting.Diagnostics: 'None'
Logging__LogLevel__Microsoft.Extensions.Hosting.Internal.Host: 'None'
Logging__LogLevel__Microsoft.EntityFrameworkCore.Database.Command: 'None'
Logging__LogLevel__JsonApiDotNetCore: 'None'
run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;annotations-title=@test (@framework);annotations-message=@error\n@trace;summary-include-passed=false"
- name: Upload coverage to codecov.io
if: ${{ matrix.os == 'ubuntu-latest' }}
Expand Down
2 changes: 1 addition & 1 deletion Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ Remove-Item -Recurse -Force * -Include coverage.cobertura.xml

dotnet tool restore
dotnet build --configuration Release
dotnet test --no-build --configuration Release --verbosity quiet --collect:"XPlat Code Coverage"
dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage"
dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage -filefilters:-*.g.cs
dotnet pack --no-build --configuration Release --output artifacts/packages
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private bool ShouldIncludeTopLevelLink(LinkTypes linkType, ResourceType? resourc

private string GetLinkForTopLevelSelf()
{
// Note: in tests, this does not properly escape special characters due to WebApplicationFactory short-circuiting.
// Note: in tests, this does not properly escape special characters due to WebApplicationFactory/TestServer short-circuiting.
return _options.UseRelativeLinks ? HttpContext.Request.GetEncodedPathAndQuery() : HttpContext.Request.GetEncodedUrl();
}

Expand Down
45 changes: 35 additions & 10 deletions test/DapperTests/IntegrationTests/DapperTestContext.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Text.Json;
using DapperExample;
using DapperExample.Data;
Expand All @@ -14,12 +15,13 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using TestBuildingBlocks;
using Xunit;
using Xunit.Abstractions;

namespace DapperTests.IntegrationTests;

[PublicAPI]
public sealed class DapperTestContext : IntegrationTest
public sealed class DapperTestContext : IntegrationTest, IAsyncLifetime
{
private const string SqlServerClearAllTablesScript = """
EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL';
Expand All @@ -31,6 +33,7 @@ public sealed class DapperTestContext : IntegrationTest

private readonly Lazy<WebApplicationFactory<TodoItem>> _lazyFactory;
private ITestOutputHelper? _testOutputHelper;
private bool _throttleAcquired;

protected override JsonSerializerOptions SerializerOptions
{
Expand All @@ -48,6 +51,12 @@ public DapperTestContext()
_lazyFactory = new Lazy<WebApplicationFactory<TodoItem>>(CreateFactory);
}

public async Task InitializeAsync()
{
await AcquireDatabaseThrottleAsync();
_throttleAcquired = true;
}

private WebApplicationFactory<TodoItem> CreateFactory()
{
#pragma warning disable CA2000 // Dispose objects before losing scope
Expand All @@ -56,7 +65,7 @@ private WebApplicationFactory<TodoItem> CreateFactory()
#pragma warning restore CA2000 // Dispose objects before losing scope
{
builder.UseSetting("ConnectionStrings:DapperExamplePostgreSql",
$"Host=localhost;Database=DapperExample-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true");
$"Host=localhost;Database=DapperExample-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true;Command Timeout=600");

builder.UseSetting("ConnectionStrings:DapperExampleMySql",
$"Host=localhost;Database=DapperExample-{Guid.NewGuid():N};User ID=root;Password=mysql;SSL Mode=None;AllowPublicKeyRetrieval=True");
Expand All @@ -68,16 +77,16 @@ private WebApplicationFactory<TodoItem> CreateFactory()

builder.ConfigureLogging(loggingBuilder =>
{
ClearLoggingProvidersInReleaseBuild(loggingBuilder);

if (_testOutputHelper != null)
{
#if !DEBUG
// Reduce logging output when running tests in ci-build.
loggingBuilder.ClearProviders();
#endif
loggingBuilder.Services.AddSingleton<ILoggerProvider>(_ => new XUnitLoggerProvider(_testOutputHelper, "DapperExample."));
}
});

builder.UseDefaultServiceProvider(ConfigureServiceProvider);

builder.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(new FrozenTimeProvider(FrozenTime)));
Expand All @@ -86,6 +95,12 @@ private WebApplicationFactory<TodoItem> CreateFactory()
});
}

[Conditional("RELEASE")]
private static void ClearLoggingProvidersInReleaseBuild(ILoggingBuilder loggingBuilder)
{
loggingBuilder.ClearProviders();
}

public void SetTestOutputHelper(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
Expand Down Expand Up @@ -123,12 +138,22 @@ public async Task ClearAllTablesAsync(DbContext dbContext)

public async Task RunOnDatabaseAsync(Func<AppDbContext, Task> asyncAction)
{
AssertThrottleAcquired();

await using AsyncServiceScope scope = Factory.Services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

await asyncAction(dbContext);
}

private void AssertThrottleAcquired()
{
if (!_throttleAcquired)
{
throw new InvalidOperationException("Database throttle not acquired.");
}
}

public string AdaptSql(string text, bool hasClientGeneratedId = false)
{
var dataModelService = Factory.Services.GetRequiredService<IDataModelService>();
Expand All @@ -141,25 +166,25 @@ protected override HttpClient CreateClient()
return Factory.CreateClient();
}

public override async Task DisposeAsync()
public async Task DisposeAsync()
{
try
{
if (_lazyFactory.IsValueCreated)
{
try
{
await RunOnDatabaseAsync(async dbContext => await dbContext.Database.EnsureDeletedAsync());
await RunOnDatabaseAsync(static async dbContext => await dbContext.Database.EnsureDeletedAsync());
}
finally
{
await _lazyFactory.Value.DisposeAsync();
await Factory.DisposeAsync();
}
}
}
finally
{
await base.DisposeAsync();
ReleaseDatabaseThrottle();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public AtomicTransactionConsistencyTests(IntegrationTestContext<TestableStartup<
services.AddResourceRepository<LyricRepository>();

string dbConnectionString =
$"Host=localhost;Database=JsonApiTest-Extra-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true";
$"Host=localhost;Database=JsonApiTest-Extra-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true;Command Timeout=600";

services.AddDbContext<ExtraDbContext>(options => options.UseNpgsql(dbConnectionString));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using JsonApiDotNetCore.OpenApi.Client.Kiota;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
Expand All @@ -21,10 +20,9 @@ public TestableHttpClientRequestAdapterFactory(ITestOutputHelper testOutputHelpe
_logHttpMessageHandler = new XUnitLogHttpMessageHandler(testOutputHelper);
}

public HttpClientRequestAdapter CreateAdapter<TStartup>(WebApplicationFactory<TStartup> webApplicationFactory)
where TStartup : class
public HttpClientRequestAdapter CreateAdapter(FactoryBridge bridge)
{
ArgumentNullException.ThrowIfNull(webApplicationFactory);
ArgumentNullException.ThrowIfNull(bridge);

DelegatingHandler[] handlers =
[
Expand All @@ -33,7 +31,7 @@ public HttpClientRequestAdapter CreateAdapter<TStartup>(WebApplicationFactory<TS
_logHttpMessageHandler
];

HttpClient httpClient = webApplicationFactory.CreateDefaultClient(handlers);
HttpClient httpClient = bridge.CreateDefaultClient(handlers);
return new HttpClientRequestAdapter(new AnonymousAuthenticationProvider(), httpClient: httpClient);
}

Expand Down
2 changes: 1 addition & 1 deletion test/OpenApiTests/OpenApiTestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace OpenApiTests;

[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public class OpenApiTestContext<TStartup, TDbContext> : IntegrationTestContext<TStartup, TDbContext>
where TStartup : class
where TStartup : IStartup, new()
where TDbContext : TestableDbContext
{
private readonly Lazy<Task<JsonElement>> _lazyDocument;
Expand Down
52 changes: 52 additions & 0 deletions test/TestBuildingBlocks/FactoryBridge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;

namespace TestBuildingBlocks;

/// <summary>
/// A temporary bridge to avoid changing all existing tests.
/// </summary>
public sealed class FactoryBridge
{
private readonly WebApplication _app;

public IServiceProvider Services => _app.Services;

internal FactoryBridge(WebApplication app)
{
ArgumentNullException.ThrowIfNull(app);

_app = app;
}

public HttpClient CreateClient()
{
return CreateDefaultClient();
}

public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
{
if (handlers.Length == 0)
{
return _app.GetTestClient();
}

TestServer testServer = _app.GetTestServer();
HttpMessageHandler serverHandler = testServer.CreateHandler();
HttpClient httpClient = CreateHttpClient(serverHandler, handlers);

httpClient.BaseAddress ??= new Uri("http://localhost");
return httpClient;
}

private static HttpClient CreateHttpClient(HttpMessageHandler serverHandler, params DelegatingHandler[] handlers)
{
for (int index = handlers.Length - 1; index > 0; index--)
{
handlers[index - 1].InnerHandler = handlers[index];
}

handlers[^1].InnerHandler = serverHandler;
return new HttpClient(handlers[0]);
}
}
11 changes: 11 additions & 0 deletions test/TestBuildingBlocks/IStartup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace TestBuildingBlocks;

public interface IStartup
{
void ConfigureServices(IServiceCollection services);

void Configure(IApplicationBuilder app);
}
39 changes: 21 additions & 18 deletions test/TestBuildingBlocks/IntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,47 @@
using System.Text.Json;
using FluentAssertions.Extensions;
using JsonApiDotNetCore.Middleware;
using Xunit;
using Microsoft.Extensions.DependencyInjection;

namespace TestBuildingBlocks;

/// <summary>
/// A base class for tests that conveniently enables executing HTTP requests against JSON:API endpoints. It throttles tests that are running in parallel
/// to avoid exceeding the maximum active database connections.
/// A base class for tests that conveniently enables executing HTTP requests against JSON:API endpoints.
/// </summary>
public abstract class IntegrationTest : IAsyncLifetime
/// <remarks>
/// Tests that use a database should call <see cref="AcquireDatabaseThrottleAsync" /> and <see cref="ReleaseDatabaseThrottle" /> to avoid exceeding the
/// maximum active database connections.
/// </remarks>
public abstract class IntegrationTest
{
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();
private static readonly SemaphoreSlim DatabaseThrottleSemaphore = CreateDatabaseThrottleSemaphore();
protected static readonly Action<ServiceProviderOptions> ConfigureServiceProvider = static options => options.ValidateScopes = true;

public static DateTimeOffset DefaultDateTimeUtc { get; } = 1.January(2020).At(1, 2, 3).AsUtc();

protected abstract JsonSerializerOptions SerializerOptions { get; }

private static SemaphoreSlim GetDefaultThrottleSemaphore()
private static SemaphoreSlim CreateDatabaseThrottleSemaphore()
{
int maxConcurrentTestRuns = OperatingSystem.IsWindows() && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSAPPIDDIR")) ? 32 : 64;
return new SemaphoreSlim(maxConcurrentTestRuns);
}

protected async Task AcquireDatabaseThrottleAsync()
{
await DatabaseThrottleSemaphore.WaitAsync();
}

protected void ReleaseDatabaseThrottle()
{
DatabaseThrottleSemaphore.Release();
}

public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteHeadAsync<TResponseDocument>(string requestUrl,
Action<HttpRequestHeaders>? setRequestHeaders = null)
{
Expand Down Expand Up @@ -104,8 +118,6 @@ private static SemaphoreSlim GetDefaultThrottleSemaphore()
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))
Expand All @@ -128,14 +140,5 @@ private static SemaphoreSlim GetDefaultThrottleSemaphore()
}
}

public async Task InitializeAsync()
{
await ThrottleSemaphore.WaitAsync();
}

public virtual Task DisposeAsync()
{
_ = ThrottleSemaphore.Release();
return Task.CompletedTask;
}
protected abstract HttpClient CreateClient();
}
Loading
Loading