2026-04-19 16:02:50 +01:00
|
|
|
using IdentityServer8.EntityFramework.Entities;
|
|
|
|
|
using IdentityServer8.Models;
|
2026-06-21 21:14:20 +01:00
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2025-07-14 18:58:04 +01:00
|
|
|
using Microsoft.AspNetCore.Builder;
|
2025-07-13 18:13:04 +01:00
|
|
|
using Microsoft.AspNetCore.Identity;
|
2026-04-19 16:02:50 +01:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
2025-07-13 18:13:04 +01:00
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
2026-07-12 06:01:42 +01:00
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Sockets;
|
2026-04-19 16:02:50 +01:00
|
|
|
using Yavsc.Extensions;
|
2026-06-22 01:44:24 +01:00
|
|
|
using Yavsc.Interfaces;
|
2026-04-19 16:02:50 +01:00
|
|
|
using Yavsc.Models;
|
2026-06-14 23:04:24 +01:00
|
|
|
using Yavsc.Server.Helpers;
|
2026-07-06 21:33:57 +01:00
|
|
|
using Yavsc.Tests.Shared;
|
2026-03-09 02:07:09 +00:00
|
|
|
using Client = IdentityServer8.EntityFramework.Entities.Client;
|
2026-06-22 01:44:24 +01:00
|
|
|
using Yavsc.Org.Tests.Fakes;
|
2025-07-13 18:13:04 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
namespace Yavsc.Org.Tests;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Specialisation of <see cref="WebHostFixture"/> for the Yavsc.Org
|
|
|
|
|
/// host. Adds:
|
|
|
|
|
/// <list type="bullet">
|
|
|
|
|
/// <item><description>In-memory configuration (<c>ConnectionStrings</c>,
|
|
|
|
|
/// <c>Smtp</c>) before <c>ConfigureWebAppServices</c> runs.</description></item>
|
|
|
|
|
/// <item><description>Test-only <see cref="TestAuthPolicyProvider"/>
|
|
|
|
|
/// (from <c>Yavsc.Tests.Shared</c>) swapped in for
|
|
|
|
|
/// <see cref="IAuthorizationPolicyProvider"/>.</description></item>
|
|
|
|
|
/// <item><description>Recording <see cref="RecordingSmtpClientFactory"/>
|
|
|
|
|
/// fake for <see cref="ISmtpClientFactory"/>.</description></item>
|
|
|
|
|
/// <item><description>IdentityServer8 client + API scope + test user
|
|
|
|
|
/// seeded into the in-memory database.</description></item>
|
|
|
|
|
/// </list>
|
|
|
|
|
/// All cross-cutting Kestrel / cert / address plumbing is inherited
|
|
|
|
|
/// from <see cref="WebHostFixture"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[CollectionDefinition("Yavsc Server")]
|
|
|
|
|
public sealed class WebServerFixture : WebHostFixture
|
2025-07-13 18:13:04 +01:00
|
|
|
{
|
2026-07-12 06:01:42 +01:00
|
|
|
private static readonly int _httpsPort = GetAvailableLoopbackPort();
|
|
|
|
|
|
test(org): isolate in-memory store per fixture
TestWebApplicationFactory instances shared the same in-memory database
because EF Core's UseInMemoryDatabase("InMemory") returns the same
backing store to every DbContext that asks for it under the same
connection string, in the same process. Whichever fixture started
first defined the state, and every subsequent fixture inherited it,
making tests silently order-dependent and flaky.
Fix:
- Yavsc.Tests.Shared/InMemoryDatabaseName: helper that suffixes the
in-memory connection string with a per-fixture GUID.
- TestWebApplicationFactory: instance GUID + ConnectionStrings__
YavscConnection set as an environment variable in the constructor
and cleared in Dispose, so each factory gets its own backing store.
Env var is needed because IdentityServer8.EntityFramework exposes
ConfigureDbContext as Action<DbContextOptionsBuilder> with no
service-provider access, so the connection string is captured at
registration time. AddEnvironmentVariables is the last provider in
the config pipeline and wins regardless.
- WebServerFixture: process-static GUID (WebHostFixture is a
per-process singleton by design, so the test collection shares one
store; the GUID still isolates from TestWebApplicationFactory).
- AddIdentityDBAndStores: read the connection string at DbContext
construction time via the (sp, options) overload of AddDbContext,
so test fixtures can override it via the host's IConfiguration.
IdentityServer stores cannot do the same without subclassing the
framework's DbContexts; the env var path is the documented escape
hatch in HostingExtensions.AddIdentityServer.
- UsesInMemoryProvider: StartsWith instead of equality, so
'InMemory-{guid}' is still recognised as an in-memory connection
string.
Regression sentinel in
Controllers/TestWebApplicationFactoryIsolationTests: two factories
seed a marker client in the first, the second must not see it.
Suite: 45/45 over 3 stable runs, 13-15s each.
2026-08-22 04:36:52 +01:00
|
|
|
// One in-memory database name for the whole process: WebHostFixture
|
|
|
|
|
// is a per-process singleton (see _app, _isInitialized, _sharedServices
|
|
|
|
|
// in the base class), so every WebServerFixture instance shares the
|
|
|
|
|
// same backing store. That is intentional — the "Yavsc Server" test
|
|
|
|
|
// collection groups tests that should see the same seeded state, and
|
|
|
|
|
// re-initialising the store per fixture would just regress the
|
|
|
|
|
// order-dependence we are trying to eliminate. The GUID still matters
|
|
|
|
|
// because TestWebApplicationFactory and WebServerFixture must not
|
|
|
|
|
// collide in the in-memory store; see InMemoryDatabaseName.
|
|
|
|
|
private static readonly string _fixtureId = Guid.NewGuid().ToString("N");
|
|
|
|
|
|
2026-07-12 06:01:42 +01:00
|
|
|
protected override int HttpsPort => _httpsPort;
|
2026-07-12 05:48:47 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
private static IConfiguration? _sharedConfiguration;
|
|
|
|
|
private static SiteSettings? _sharedSiteSettings;
|
|
|
|
|
private static ILogger? _sharedLogger;
|
|
|
|
|
private static string? _sharedTestClientId;
|
|
|
|
|
private static string? _sharedTestClientSecret;
|
|
|
|
|
private static string? _sharedTestingUserName;
|
|
|
|
|
private static string? _sharedTestingUserPassword;
|
|
|
|
|
private static string? _sharedTestingUserEmail;
|
|
|
|
|
private static RecordingSmtpClientFactory? _sharedSmtpClientFactory;
|
|
|
|
|
|
|
|
|
|
public IConfiguration? Configuration { get; private set; }
|
|
|
|
|
public string? TestClientId { get; private set; }
|
|
|
|
|
public string? TestClientSecret { get; set; }
|
|
|
|
|
public string? TestingUserName { get; private set; }
|
|
|
|
|
public string? TestingUserPassword { get; private set; }
|
|
|
|
|
public string? TestingUserEmail { get; set; }
|
|
|
|
|
public string? ProtectedTestingApiKey { get; internal set; }
|
|
|
|
|
public ApplicationUser? TestingUser { get; private set; }
|
|
|
|
|
public bool DbCreated { get; internal set; }
|
|
|
|
|
public SiteSettings? SiteSettings { get; set; }
|
|
|
|
|
public RecordingSmtpClientFactory? SmtpClientFactory { get; private set; }
|
|
|
|
|
public ILogger? Logger { get; internal set; }
|
|
|
|
|
|
|
|
|
|
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
2025-07-13 18:13:04 +01:00
|
|
|
{
|
2026-07-12 06:01:42 +01:00
|
|
|
var authority = $"https://localhost:{_httpsPort}";
|
|
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
// WebApplication.CreateBuilder defaults WebRootPath to
|
|
|
|
|
// {ContentRoot}/wwwroot. The test assembly runs from
|
|
|
|
|
// src/Yavsc.Org.Tests/bin/.../, which has no wwwroot of
|
|
|
|
|
// its own — so point the host at the Yavsc.Org project's
|
|
|
|
|
// wwwroot so that ConfigurePipeline's static-assets middleware
|
|
|
|
|
// can resolve it. The AddConfiguration extension takes care of
|
|
|
|
|
// that plus the in-memory overrides below.
|
|
|
|
|
builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary<string, string?>
|
2026-08-20 20:50:52 +01:00
|
|
|
{
|
test(org): isolate in-memory store per fixture
TestWebApplicationFactory instances shared the same in-memory database
because EF Core's UseInMemoryDatabase("InMemory") returns the same
backing store to every DbContext that asks for it under the same
connection string, in the same process. Whichever fixture started
first defined the state, and every subsequent fixture inherited it,
making tests silently order-dependent and flaky.
Fix:
- Yavsc.Tests.Shared/InMemoryDatabaseName: helper that suffixes the
in-memory connection string with a per-fixture GUID.
- TestWebApplicationFactory: instance GUID + ConnectionStrings__
YavscConnection set as an environment variable in the constructor
and cleared in Dispose, so each factory gets its own backing store.
Env var is needed because IdentityServer8.EntityFramework exposes
ConfigureDbContext as Action<DbContextOptionsBuilder> with no
service-provider access, so the connection string is captured at
registration time. AddEnvironmentVariables is the last provider in
the config pipeline and wins regardless.
- WebServerFixture: process-static GUID (WebHostFixture is a
per-process singleton by design, so the test collection shares one
store; the GUID still isolates from TestWebApplicationFactory).
- AddIdentityDBAndStores: read the connection string at DbContext
construction time via the (sp, options) overload of AddDbContext,
so test fixtures can override it via the host's IConfiguration.
IdentityServer stores cannot do the same without subclassing the
framework's DbContexts; the env var path is the documented escape
hatch in HostingExtensions.AddIdentityServer.
- UsesInMemoryProvider: StartsWith instead of equality, so
'InMemory-{guid}' is still recognised as an in-memory connection
string.
Regression sentinel in
Controllers/TestWebApplicationFactoryIsolationTests: two factories
seed a marker client in the first, the second must not see it.
Suite: 45/45 over 3 stable runs, 13-15s each.
2026-08-22 04:36:52 +01:00
|
|
|
[$"ConnectionStrings:{Yavsc.Constants.YavscConnectionStringName}"] = InMemoryDatabaseName.For(_fixtureId),
|
2026-07-06 21:33:57 +01:00
|
|
|
// SMTP test config: UserName non-null so MailSender
|
|
|
|
|
// exercises the Authenticate branch — the
|
|
|
|
|
// RecordingSmtpClient captures it.
|
|
|
|
|
["Smtp:Host"] = "smtp.test.local",
|
|
|
|
|
["Smtp:Port"] = "465",
|
|
|
|
|
["Smtp:UserName"] = "test-user",
|
|
|
|
|
["Smtp:Password"] = "test-pass",
|
2026-07-12 06:01:42 +01:00
|
|
|
["Site:Authority"] = authority
|
2026-07-06 21:33:57 +01:00
|
|
|
});
|
2025-07-13 18:13:04 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
Configuration = builder.Configuration;
|
|
|
|
|
|
|
|
|
|
// Swap the production authorization policy provider for
|
|
|
|
|
// TestAuthPolicyProvider BEFORE ConfigureWebAppServices
|
|
|
|
|
// runs. ConfigureWebAppServices calls builder.Build() at
|
|
|
|
|
// the end, which freezes the service collection. Tests
|
|
|
|
|
// can satisfy [Authorize("AdministratorOnly")] (and any
|
|
|
|
|
// other policy that requires a role) by sending an
|
|
|
|
|
// X-Test-Role header; the production policy is replaced
|
|
|
|
|
// by the test one via the last-write-wins semantics of
|
|
|
|
|
// IServiceCollection.AddSingleton.
|
|
|
|
|
builder.Services.AddSingleton<IAuthorizationPolicyProvider, TestAuthPolicyProvider>();
|
|
|
|
|
|
|
|
|
|
// Replace the production ISmtpClientFactory (added later
|
|
|
|
|
// by ConfigureWebAppServices via TryAddSingleton) with a
|
|
|
|
|
// recording fake. By pre-registering here, the prod
|
|
|
|
|
// TryAdd becomes a no-op and tests get a single shared
|
|
|
|
|
// fake they can assert against.
|
|
|
|
|
var smtpFactory = new RecordingSmtpClientFactory();
|
|
|
|
|
builder.Services.AddSingleton<ISmtpClientFactory>(smtpFactory);
|
|
|
|
|
_sharedSmtpClientFactory = smtpFactory;
|
|
|
|
|
|
|
|
|
|
var app = builder.ConfigureWebAppServices();
|
|
|
|
|
SiteSettings = app.Services.GetRequiredService<IOptions<SiteSettings>>().Value;
|
|
|
|
|
|
|
|
|
|
using (var migrationScope = app.Services.CreateScope())
|
2025-07-13 18:13:04 +01:00
|
|
|
{
|
2026-07-06 21:33:57 +01:00
|
|
|
var db = migrationScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
|
|
|
db.Database.EnsureDeleted();
|
|
|
|
|
db.Database.EnsureCreated();
|
|
|
|
|
TestingUserName = "Tester";
|
|
|
|
|
TestingUserPassword = "Test123!";
|
|
|
|
|
TestClientId = "testClientId";
|
|
|
|
|
TestingUserEmail = "test@no-reply.com";
|
|
|
|
|
TestingUser = null;
|
|
|
|
|
TestClientSecret = Guid.CreateVersion7().ToString();
|
|
|
|
|
EnsureUser(TestingUserName, TestingUserPassword, TestingUserEmail, migrationScope);
|
|
|
|
|
AddAuthorizedClient(migrationScope, TestClientId, TestClientSecret);
|
|
|
|
|
TestingUser = db.Users.FirstOrDefaultAsync(u => u.UserName == TestingUserName).Result;
|
|
|
|
|
|
|
|
|
|
// Add test API scope if it doesn't exist
|
|
|
|
|
var testScope = db.ApiScopes.FirstOrDefault(s => s.Name == "test");
|
|
|
|
|
if (testScope == null)
|
2026-04-19 16:18:37 +01:00
|
|
|
{
|
2026-07-06 21:33:57 +01:00
|
|
|
db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope
|
2026-05-28 22:18:26 +01:00
|
|
|
{
|
2026-07-06 21:33:57 +01:00
|
|
|
Name = "test",
|
|
|
|
|
Enabled = true,
|
|
|
|
|
DisplayName = "Test API Scope",
|
|
|
|
|
Description = "Scope for testing purposes",
|
|
|
|
|
UserClaims = new List<IdentityServer8.EntityFramework.Entities.ApiScopeClaim>
|
|
|
|
|
{
|
|
|
|
|
new IdentityServer8.EntityFramework.Entities.ApiScopeClaim { Type = "role" },
|
|
|
|
|
new IdentityServer8.EntityFramework.Entities.ApiScopeClaim { Type = "email" }
|
|
|
|
|
}
|
2026-05-28 22:18:26 +01:00
|
|
|
});
|
2026-04-19 16:02:50 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
// Add a basic API resource for the test scope
|
|
|
|
|
var apiResource = new IdentityServer8.EntityFramework.Entities.ApiResource
|
2026-04-19 16:18:37 +01:00
|
|
|
{
|
2026-07-06 21:33:57 +01:00
|
|
|
Name = "testapi",
|
|
|
|
|
DisplayName = "Test API",
|
|
|
|
|
Enabled = true,
|
|
|
|
|
Scopes = new List<IdentityServer8.EntityFramework.Entities.ApiResourceScope>
|
|
|
|
|
{
|
|
|
|
|
new IdentityServer8.EntityFramework.Entities.ApiResourceScope { Scope = "test" }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
db.ApiResources.Add(apiResource);
|
|
|
|
|
db.SaveChanges();
|
2026-04-19 18:10:00 +01:00
|
|
|
}
|
2026-07-06 21:33:57 +01:00
|
|
|
}
|
2026-04-19 18:10:00 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
_sharedConfiguration = Configuration;
|
|
|
|
|
_sharedSiteSettings = SiteSettings;
|
|
|
|
|
_sharedTestClientId = TestClientId;
|
|
|
|
|
_sharedTestClientSecret = TestClientSecret;
|
|
|
|
|
_sharedTestingUserName = TestingUserName;
|
|
|
|
|
_sharedTestingUserPassword = TestingUserPassword;
|
|
|
|
|
_sharedTestingUserEmail = TestingUserEmail;
|
|
|
|
|
_sharedLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<WebServerFixture>();
|
|
|
|
|
Logger = _sharedLogger;
|
|
|
|
|
SmtpClientFactory = smtpFactory;
|
|
|
|
|
|
|
|
|
|
return app;
|
|
|
|
|
}
|
2026-06-14 23:04:24 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
protected override async Task<WebApplication> ConfigurePipelineAsync(WebApplication app)
|
|
|
|
|
{
|
|
|
|
|
// The MSBuild target CopyYavscOrgStaticAssets in
|
|
|
|
|
// Yavsc.Org.Tests.csproj mirrors the Yavsc.Org static
|
|
|
|
|
// assets manifest into the test bin directory. Call
|
|
|
|
|
// MapStaticAssets() with the explicit path so the test
|
|
|
|
|
// host resolves the manifest by file location rather
|
|
|
|
|
// than by {AssemblyName}.staticwebassets.* convention
|
|
|
|
|
// (which would look for Yavsc.Org.Tests.staticwebassets.*,
|
|
|
|
|
// a file we don't produce).
|
|
|
|
|
var testRuntimeManifest = Path.Combine(AppContext.BaseDirectory,
|
|
|
|
|
"Yavsc.Org.staticwebassets.runtime.json");
|
|
|
|
|
return await app.ConfigurePipeline(testRuntimeManifest);
|
|
|
|
|
}
|
2026-06-21 21:14:20 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
protected override void CopySpecialisedSharedState()
|
|
|
|
|
{
|
|
|
|
|
TestClientId = _sharedTestClientId;
|
|
|
|
|
TestClientSecret = _sharedTestClientSecret;
|
|
|
|
|
TestingUserName = _sharedTestingUserName;
|
|
|
|
|
TestingUserPassword = _sharedTestingUserPassword;
|
|
|
|
|
TestingUserEmail = _sharedTestingUserEmail;
|
|
|
|
|
SmtpClientFactory = _sharedSmtpClientFactory;
|
|
|
|
|
Configuration = _sharedConfiguration;
|
|
|
|
|
SiteSettings = _sharedSiteSettings;
|
|
|
|
|
Logger = _sharedLogger;
|
|
|
|
|
}
|
2025-07-15 19:43:41 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
private void AddAuthorizedClient(IServiceScope scope, string testClientId, string testClientSecret)
|
|
|
|
|
{
|
|
|
|
|
var configDb = scope.ServiceProvider.GetRequiredService<IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext>();
|
|
|
|
|
if (configDb == null)
|
|
|
|
|
throw new InvalidOperationException("ConfigurationDbContext is not available for IdentityServer client seeding.");
|
2025-07-13 18:13:04 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
Client testingClient = new Client
|
|
|
|
|
{
|
|
|
|
|
ClientId = testClientId,
|
|
|
|
|
AccessTokenLifetime = 3600000,
|
|
|
|
|
AccessTokenType = 1,
|
|
|
|
|
ClientName = "Testing client",
|
|
|
|
|
Enabled = true,
|
|
|
|
|
RequireClientSecret = true
|
|
|
|
|
};
|
|
|
|
|
configDb.Set<Client>().Add(testingClient);
|
|
|
|
|
configDb.SaveChanges();
|
|
|
|
|
|
|
|
|
|
var apiScope = new IdentityServer8.EntityFramework.Entities.ApiScope
|
|
|
|
|
{
|
|
|
|
|
Name = "test",
|
|
|
|
|
DisplayName = "Test Scope",
|
|
|
|
|
Description = "Scope for testing",
|
|
|
|
|
Enabled = true,
|
|
|
|
|
Required = false,
|
|
|
|
|
ShowInDiscoveryDocument = true,
|
|
|
|
|
Emphasize = false
|
|
|
|
|
};
|
|
|
|
|
configDb.Set<IdentityServer8.EntityFramework.Entities.ApiScope>().Add(apiScope);
|
|
|
|
|
|
|
|
|
|
ClientSecret secret = new ClientSecret
|
|
|
|
|
{
|
|
|
|
|
Value = testClientSecret.Sha256(),
|
|
|
|
|
Type = IdentityServer8.IdentityServerConstants.SecretTypes.SharedSecret,
|
|
|
|
|
ClientId = testingClient.Id
|
|
|
|
|
};
|
|
|
|
|
configDb.Set<ClientSecret>().Add(secret);
|
2026-03-09 02:07:09 +00:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
configDb.Set<ClientGrantType>().Add(new ClientGrantType
|
|
|
|
|
{
|
|
|
|
|
ClientId = testingClient.Id,
|
|
|
|
|
GrantType = "client_credentials"
|
|
|
|
|
});
|
|
|
|
|
configDb.Set<ClientGrantType>().Add(new ClientGrantType
|
|
|
|
|
{
|
|
|
|
|
ClientId = testingClient.Id,
|
|
|
|
|
GrantType = "password"
|
|
|
|
|
});
|
|
|
|
|
configDb.Set<ClientScope>().Add(new ClientScope
|
2026-03-09 02:07:09 +00:00
|
|
|
{
|
2026-07-06 21:33:57 +01:00
|
|
|
ClientId = testingClient.Id,
|
|
|
|
|
Scope = "test"
|
|
|
|
|
});
|
2026-04-19 17:15:10 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
configDb.SaveChanges();
|
|
|
|
|
}
|
2026-04-19 19:14:37 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
public void EnsureUser(string testingUserName, string password, string email, IServiceScope scope)
|
|
|
|
|
{
|
|
|
|
|
if (TestingUser == null)
|
|
|
|
|
{
|
|
|
|
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
2026-04-19 20:36:03 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
TestingUser = new ApplicationUser
|
2026-04-19 17:15:10 +01:00
|
|
|
{
|
2026-07-06 21:33:57 +01:00
|
|
|
UserName = testingUserName,
|
|
|
|
|
Email = testingUserName + "@example.com",
|
|
|
|
|
EmailConfirmed = true
|
2026-04-19 17:15:10 +01:00
|
|
|
};
|
2026-04-19 16:18:37 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
var result = userManager.CreateAsync(TestingUser, password).Result;
|
2026-04-19 16:18:37 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
Assert.True(result.Succeeded);
|
2026-04-19 16:18:37 +01:00
|
|
|
|
2026-07-06 21:33:57 +01:00
|
|
|
ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
|
|
|
TestingUser = dbContext.Users.FirstOrDefault(u => u.UserName == testingUserName);
|
2026-04-19 16:18:37 +01:00
|
|
|
}
|
2025-07-13 18:13:04 +01:00
|
|
|
}
|
2026-07-12 06:01:42 +01:00
|
|
|
|
|
|
|
|
private static int GetAvailableLoopbackPort()
|
|
|
|
|
{
|
|
|
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
|
|
|
listener.Start();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listener.Stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-13 18:13:04 +01:00
|
|
|
}
|