parent
fa7794b7a0
commit
cb20b8a2d5
10 changed files with 18 additions and 169 deletions
|
|
@ -18,7 +18,6 @@
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Razor" Version="2.3.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Razor" Version="2.3.0" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,10 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reset the in-memory database to a known empty state.
|
/// <summary>Reset the in-memory database to a known empty state.
|
||||||
/// The fixture now uses SQLite in-memory (see forgejo#3), which
|
/// <c>UseInMemoryDatabase</c> shares its store across the
|
||||||
/// shares its store across the lifetime of the
|
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
|
||||||
/// <see cref="BlogsWebServerFixture"/> instance, so without a
|
/// so without a per-test reset the test order would leak
|
||||||
/// per-test reset the test order would leak state between
|
/// state between tests.</summary>
|
||||||
/// tests.</summary>
|
|
||||||
private void ResetDatabase()
|
private void ResetDatabase()
|
||||||
{
|
{
|
||||||
using var scope = _fixture.Services.CreateScope();
|
using var scope = _fixture.Services.CreateScope();
|
||||||
|
|
|
||||||
|
|
@ -48,19 +48,6 @@ namespace Yavsc.Blogs.Tests;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BlogsWebServerFixture : WebHostFixture
|
public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
{
|
{
|
||||||
// SQLite in-memory database is created once and shared across all
|
|
||||||
// DbContext instances for the test lifetime. The connection must
|
|
||||||
// stay open: closing it destroys the in-memory database. The
|
|
||||||
// Microsoft.Data.Sqlite pool will then open additional connections
|
|
||||||
// to the same in-memory store, as long as the original connection
|
|
||||||
// is alive. This is the SQLite equivalent of the EF Core
|
|
||||||
// InMemoryDatabaseRoot we used to use.
|
|
||||||
private Microsoft.Data.Sqlite.SqliteConnection? _sharedSqliteConnection;
|
|
||||||
// Legacy field kept to make the migration diff readable. The
|
|
||||||
// InMemory provider path is no longer used by this fixture, but
|
|
||||||
// removing it is out of scope for the SQLite-in-memory migration
|
|
||||||
// (forgejo#3 follow-up).
|
|
||||||
[System.Obsolete("Replaced by SQLite in-memory (forgejo#3).")]
|
|
||||||
private InMemoryDatabaseRoot? _inMemoryRoot;
|
private InMemoryDatabaseRoot? _inMemoryRoot;
|
||||||
|
|
||||||
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
||||||
|
|
@ -71,18 +58,15 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
// against an empty table returns an empty list, which is
|
// against an empty table returns an empty list, which is
|
||||||
// exactly what the first test wants to assert.
|
// exactly what the first test wants to assert.
|
||||||
//
|
//
|
||||||
// We use SQLite in-memory (not the EF Core InMemory provider)
|
// Share a single InMemoryDatabaseRoot across the test
|
||||||
// because the InMemory provider cannot materialise navigation
|
// lifetime so POST + GET on the same fixture see the same
|
||||||
// properties from IdentityServer8 entity types (see forgejo#3).
|
// store. Without the root, EF Core's In-Memory provider
|
||||||
// SQLite in-memory is a transient, file-less store that
|
// creates independent stores per DbContext in some
|
||||||
// executes real SQL, so navigation properties work as
|
// configurations, and the second request would see an
|
||||||
// expected. The shared SqliteConnection keeps the database
|
// empty list even after the first wrote a row.
|
||||||
// alive for the test lifetime, mirroring the
|
_inMemoryRoot = new InMemoryDatabaseRoot();
|
||||||
// InMemoryDatabaseRoot pattern we used previously.
|
|
||||||
_sharedSqliteConnection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
|
|
||||||
_sharedSqliteConnection.Open();
|
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
||||||
opt.UseSqlite(_sharedSqliteConnection));
|
opt.UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot));
|
||||||
|
|
||||||
// Trivial file-system auth: the GET index path never calls
|
// Trivial file-system auth: the GET index path never calls
|
||||||
// into it, but the DI container needs an instance.
|
// into it, but the DI container needs an instance.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
|
||||||
<PackageReference Include="xunit.v3" />
|
<PackageReference Include="xunit.v3" />
|
||||||
<PackageReference Include="xunit.v3.common" />
|
<PackageReference Include="xunit.v3.common" />
|
||||||
<PackageReference Include="xunit.v3.extensibility.core" />
|
<PackageReference Include="xunit.v3.extensibility.core" />
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Yavsc.Models;
|
|
||||||
using Yavsc.Tests.Shared;
|
using Yavsc.Tests.Shared;
|
||||||
|
|
||||||
namespace Yavsc.Org.Tests;
|
namespace Yavsc.Org.Tests;
|
||||||
|
|
@ -28,21 +24,6 @@ namespace Yavsc.Org.Tests;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TestWebApplicationFactory : WebApplicationFactory<Program>
|
public class TestWebApplicationFactory : WebApplicationFactory<Program>
|
||||||
{
|
{
|
||||||
// SQLite in-memory: the connection must stay open for the lifetime
|
|
||||||
// of the host, otherwise the in-memory database is destroyed and
|
|
||||||
// every new DbContext sees an empty store. We hold the connection
|
|
||||||
// here so it is disposed only when the factory is disposed. The
|
|
||||||
// Microsoft.Data.Sqlite pool reuses the underlying in-memory store
|
|
||||||
// across additional connections opened against the same connection
|
|
||||||
// string, as long as the original connection is alive. This is the
|
|
||||||
// SQLite equivalent of the EF Core InMemoryDatabaseRoot pattern.
|
|
||||||
private readonly SqliteConnection _sharedSqliteConnection = new("Data Source=:memory:");
|
|
||||||
|
|
||||||
public TestWebApplicationFactory()
|
|
||||||
{
|
|
||||||
_sharedSqliteConnection.Open();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
// UseEnvironment("Testing") puts the host in a dedicated
|
// UseEnvironment("Testing") puts the host in a dedicated
|
||||||
|
|
@ -57,25 +38,6 @@ public class TestWebApplicationFactory : WebApplicationFactory<Program>
|
||||||
|
|
||||||
builder.ConfigureTestServices(services =>
|
builder.ConfigureTestServices(services =>
|
||||||
{
|
{
|
||||||
// The production Program.Main calls AddConfiguration("org")
|
|
||||||
// and then AddIdentityDBAndStores which calls
|
|
||||||
// GetConnectionString("YavscConnection"). The result is
|
|
||||||
// "Data Source=:memory:" (from appsettings-org.Testing.json),
|
|
||||||
// and the production code path in HostingExtensions routes
|
|
||||||
// that to UseSqlite. However, the EF Core in-memory test
|
|
||||||
// pattern needs all DbContext instances to see the same
|
|
||||||
// store; with a raw "Data Source=:memory:" connection string,
|
|
||||||
// each connection opens its own private database. We
|
|
||||||
// therefore drop the production DbContext registration and
|
|
||||||
// re-register ApplicationDbContext with the shared
|
|
||||||
// SqliteConnection held by this factory. Tests that need
|
|
||||||
// the schema to exist call EnsureCreated on the resulting
|
|
||||||
// DbContext (e.g. ClientControllerCollectionTests seeds a
|
|
||||||
// Client row in its constructor).
|
|
||||||
services.RemoveAll<DbContextOptions<ApplicationDbContext>>();
|
|
||||||
services.AddDbContext<ApplicationDbContext>(opt =>
|
|
||||||
opt.UseSqlite(_sharedSqliteConnection));
|
|
||||||
|
|
||||||
// Replace the production IAuthorizationPolicyProvider with
|
// Replace the production IAuthorizationPolicyProvider with
|
||||||
// the test one. The default registered by AddAuthorization
|
// the test one. The default registered by AddAuthorization
|
||||||
// becomes irrelevant: any GetPolicyAsync call is routed here.
|
// becomes irrelevant: any GetPolicyAsync call is routed here.
|
||||||
|
|
@ -86,37 +48,6 @@ public class TestWebApplicationFactory : WebApplicationFactory<Program>
|
||||||
// TestUserMiddleware runs after UseAuthentication/Authorization.
|
// TestUserMiddleware runs after UseAuthentication/Authorization.
|
||||||
services.AddTransient<TestUserMiddleware>();
|
services.AddTransient<TestUserMiddleware>();
|
||||||
services.AddTransient<IStartupFilter, TestUserStartupFilter>();
|
services.AddTransient<IStartupFilter, TestUserStartupFilter>();
|
||||||
|
|
||||||
// Run EnsureCreated once at host start. With SQLite in-memory
|
|
||||||
// and a shared connection, this creates the schema once
|
|
||||||
// and the schema persists for the host lifetime. The
|
|
||||||
// test code (e.g. ClientControllerCollectionTests seed) can
|
|
||||||
// then write rows without having to call EnsureCreated
|
|
||||||
// itself. EnsureCreated is idempotent: re-running it on
|
|
||||||
// an existing schema is a no-op.
|
|
||||||
services.AddHostedService<SqliteEnsureCreatedHostedService>();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SqliteEnsureCreatedHostedService : IHostedService
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _services;
|
|
||||||
public SqliteEnsureCreatedHostedService(IServiceProvider services)
|
|
||||||
{
|
|
||||||
_services = services;
|
|
||||||
}
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var scope = _services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
return db.Database.EnsureCreatedAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing) _sharedSqliteConnection.Dispose();
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,20 +75,7 @@ public sealed class WebServerFixture : WebHostFixture
|
||||||
// that plus the in-memory overrides below.
|
// that plus the in-memory overrides below.
|
||||||
builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary<string, string?>
|
builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
// The EF Core in-memory provider cannot materialise
|
[$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory",
|
||||||
// entity types from IdentityServer8 (see forgejo#3):
|
|
||||||
// it crashes with IndexOutOfRangeException on the
|
|
||||||
// multi-Include query in ClientController.LoadClientAsync
|
|
||||||
// and on per-collection LoadAsync. SQLite in-memory is
|
|
||||||
// a transient, file-less store that uses the same
|
|
||||||
// connection string semantics as the InMemory provider
|
|
||||||
// ("keep the connection open for the host lifetime")
|
|
||||||
// but actually executes SQL, so it handles
|
|
||||||
// navigation-property entities correctly. The
|
|
||||||
// HostingExtensions code path detects this connection
|
|
||||||
// string and routes to UseSqlite. See
|
|
||||||
// doc/testing.md for the test-driver policy.
|
|
||||||
[$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "Data Source=:memory:",
|
|
||||||
// SMTP test config: UserName non-null so MailSender
|
// SMTP test config: UserName non-null so MailSender
|
||||||
// exercises the Authenticate branch — the
|
// exercises the Authenticate branch — the
|
||||||
// RecordingSmtpClient captures it.
|
// RecordingSmtpClient captures it.
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!--
|
<!--
|
||||||
MapStaticAssets() in the production pipeline resolves
|
MapStaticAssets() in the production pipeline resolves
|
||||||
|
|
|
||||||
|
|
@ -174,22 +174,8 @@ public static class HostingExtensions
|
||||||
|
|
||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
{
|
{
|
||||||
if (UsesSqliteInMemoryProvider(connectionString))
|
if (UsesInMemoryProvider(connectionString))
|
||||||
{
|
{
|
||||||
// The Sqlite connection must be kept open for the
|
|
||||||
// lifetime of the host: closing the connection
|
|
||||||
// destroys the in-memory database. The fixture
|
|
||||||
// manages this; the provider just receives the
|
|
||||||
// connection string and opens its own pooled
|
|
||||||
// connections.
|
|
||||||
options.UseSqlite(connectionString);
|
|
||||||
}
|
|
||||||
else if (UsesInMemoryProvider(connectionString))
|
|
||||||
{
|
|
||||||
// Legacy in-memory provider (Microsoft.EntityFrameworkCore.InMemory).
|
|
||||||
// Kept for tests that do not exercise navigation
|
|
||||||
// properties from IdentityServer8 entity types. The
|
|
||||||
// general case is SQLite in-memory, above.
|
|
||||||
options.UseInMemoryDatabase(connectionString);
|
options.UseInMemoryDatabase(connectionString);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -357,11 +343,7 @@ public static class HostingExtensions
|
||||||
{
|
{
|
||||||
options.ConfigureDbContext = b =>
|
options.ConfigureDbContext = b =>
|
||||||
{
|
{
|
||||||
if (UsesSqliteInMemoryProvider(connectionString))
|
if (UsesInMemoryProvider(connectionString))
|
||||||
{
|
|
||||||
b.UseSqlite(connectionString);
|
|
||||||
}
|
|
||||||
else if (UsesInMemoryProvider(connectionString))
|
|
||||||
{
|
{
|
||||||
b.UseInMemoryDatabase(connectionString);
|
b.UseInMemoryDatabase(connectionString);
|
||||||
}
|
}
|
||||||
|
|
@ -385,11 +367,7 @@ public static class HostingExtensions
|
||||||
{
|
{
|
||||||
options.ConfigureDbContext = b =>
|
options.ConfigureDbContext = b =>
|
||||||
{
|
{
|
||||||
if (UsesSqliteInMemoryProvider(connectionString))
|
if (UsesInMemoryProvider(connectionString))
|
||||||
{
|
|
||||||
b.UseSqlite(connectionString);
|
|
||||||
}
|
|
||||||
else if (UsesInMemoryProvider(connectionString))
|
|
||||||
{
|
{
|
||||||
b.UseInMemoryDatabase(connectionString);
|
b.UseInMemoryDatabase(connectionString);
|
||||||
}
|
}
|
||||||
|
|
@ -623,35 +601,9 @@ public static class HostingExtensions
|
||||||
|
|
||||||
private static bool UsesInMemoryProvider(string connectionString)
|
private static bool UsesInMemoryProvider(string connectionString)
|
||||||
{
|
{
|
||||||
// The legacy in-memory marker (EF Core InMemory provider).
|
return string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase);
|
||||||
if (string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return true;
|
|
||||||
// SQLite in-memory is a separate driver (Microsoft.EntityFrameworkCore.Sqlite)
|
|
||||||
// but is conceptually the same: a transient, file-less store
|
|
||||||
// that lives only for the duration of a test. Tests use it for
|
|
||||||
// entity types (IdentityServer8 navigation properties) that the
|
|
||||||
// InMemory provider cannot materialise. The check is intentionally
|
|
||||||
// liberal: any SQLite connection string that opens an in-memory
|
|
||||||
// database qualifies.
|
|
||||||
return IsSqliteInMemoryConnectionString(connectionString);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSqliteInMemoryConnectionString(string connectionString)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(connectionString)) return false;
|
|
||||||
// The canonical Microsoft.Data.Sqlite form: "Data Source=:memory:"
|
|
||||||
// (with or without spaces around the colon). The keyword is
|
|
||||||
// case-insensitive.
|
|
||||||
if (connectionString.Contains("Data Source=:memory:", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return true;
|
|
||||||
if (connectionString.Contains("DataSource=:memory:", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool UsesSqliteInMemoryProvider(string connectionString)
|
|
||||||
=> IsSqliteInMemoryConnectionString(connectionString);
|
|
||||||
|
|
||||||
private static Action<DbContext, bool> EnsureDefaultApplicationScopes()
|
private static Action<DbContext, bool> EnsureDefaultApplicationScopes()
|
||||||
{
|
{
|
||||||
return (context, _) =>
|
return (context, _) =>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||||
<PackageReference Include="Google.Apis.Compute.v1" />
|
<PackageReference Include="Google.Apis.Compute.v1" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"YavscConnection": "Data Source=:memory:"
|
"YavscConnection": "InMemory"
|
||||||
},
|
},
|
||||||
"Smtp": {
|
"Smtp": {
|
||||||
"Host": "smtp.test.local",
|
"Host": "smtp.test.local",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue