using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Time.Testing; using NeonSprawl.Server.Game.Combat; using NeonSprawl.Server.Game.Crafting; using NeonSprawl.Server.Game.Gathering; using NeonSprawl.Server.Game.Items; using NeonSprawl.Server.Game.Mastery; namespace NeonSprawl.Server.Tests.Game.PositionState; /// Integration host with bound from environment. public sealed class PostgresWebApplicationFactory : WebApplicationFactory { /// Controllable UTC clock for cast cooldown integration tests (NEO-32). public FakeTimeProvider? FakeClock { get; private set; } protected override void ConfigureWebHost(IWebHostBuilder builder) { var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl"); PostgresDockerHelper.EnsurePostgresOrStartDockerBlocking(cs); if (string.IsNullOrWhiteSpace(cs)) { throw new InvalidOperationException( "Set ConnectionStrings__NeonSprawl for Postgres integration tests (see server/README.md)."); } builder.UseEnvironment("Testing"); var skillsDir = SkillCatalogPathResolution.TryDiscoverSkillsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/skills from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var masteryDir = MasteryCatalogPathResolution.TryDiscoverMasteryDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/mastery from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var itemsDir = ItemCatalogPathResolution.TryDiscoverItemsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/items from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var resourceNodesDir = ResourceNodeCatalogPathResolution.TryDiscoverResourceNodesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/resource-nodes from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var recipesDir = RecipeCatalogPathResolution.TryDiscoverRecipesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/recipes from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var abilitiesDir = AbilityCatalogPathResolution.TryDiscoverAbilitiesDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/abilities from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); var npcBehaviorsDir = NpcBehaviorCatalogPathResolution.TryDiscoverNpcBehaviorsDirectory(AppContext.BaseDirectory) ?? throw new InvalidOperationException( "Could not discover repo content/npc-behaviors from AppContext.BaseDirectory; run tests from the neon-sprawl clone."); builder.UseSetting("Content:SkillsDirectory", skillsDir); builder.UseSetting("Content:MasteryDirectory", masteryDir); builder.UseSetting("Content:ItemsDirectory", itemsDir); builder.UseSetting("Content:ResourceNodesDirectory", resourceNodesDir); builder.UseSetting("Content:RecipesDirectory", recipesDir); builder.UseSetting("Content:AbilitiesDirectory", abilitiesDir); builder.UseSetting("Content:NpcBehaviorsDirectory", npcBehaviorsDir); builder.ConfigureAppConfiguration((_, config) => { config.AddInMemoryCollection(new Dictionary { ["ConnectionStrings:NeonSprawl"] = cs.Trim(), }); }); builder.ConfigureTestServices(services => { for (var i = services.Count - 1; i >= 0; i--) { if (services[i].ServiceType == typeof(TimeProvider)) { services.RemoveAt(i); } } var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 4, 30, 12, 0, 0, TimeSpan.Zero)); FakeClock = fakeTime; services.AddSingleton(fakeTime); }); } }