From acffbc199d41800463e5443ae538c570fa85721f Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 8 Feb 2026 21:46:26 +0100 Subject: [PATCH] Remove startup migration and runtime frontend rewrites --- GameList.Tests/HelperTests.cs | 41 ++--------------- .../Support/TestWebApplicationFactory.cs | 3 +- IIS.md | 1 + Program.cs | 46 ------------------- README.md | 9 ++-- TESTS.md | 2 +- 6 files changed, 12 insertions(+), 90 deletions(-) diff --git a/GameList.Tests/HelperTests.cs b/GameList.Tests/HelperTests.cs index 4e60309..5533a73 100644 --- a/GameList.Tests/HelperTests.cs +++ b/GameList.Tests/HelperTests.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Configuration; using System.Text.Json; using System.Net.Http.Json; @@ -28,34 +27,10 @@ public class HelperTests } [Fact] - public void UpdateIndexMetaBase_rewrites_content_value() + public void Program_does_not_include_runtime_index_rewrite_hook() { - var webRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(webRoot); - var index = Path.Combine(webRoot, "index.html"); - File.WriteAllText(index, ""); - - var env = new FakeEnv { WebRootPath = webRoot }; - var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).First(m => m.Name.Contains("UpdateIndexMetaBase")); - method.Invoke(null, [env, "/pick"]); - - var text = File.ReadAllText(index); - Assert.Contains("content=\"/pick\"", text); - } - - [Fact] - public void UpdateIndexMetaBase_no_marker_no_change() - { - var webRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(webRoot); - var index = Path.Combine(webRoot, "index.html"); - File.WriteAllText(index, ""); - - var env = new FakeEnv { WebRootPath = webRoot }; - var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).First(m => m.Name.Contains("UpdateIndexMetaBase")); - method.Invoke(null, [env, "/pick"]); - - Assert.Equal("", File.ReadAllText(index)); + var hasRewriteMethod = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Any(m => m.Name.Contains("UpdateIndexMetaBase", StringComparison.Ordinal)); + Assert.False(hasRewriteMethod); } [Fact] @@ -349,16 +324,6 @@ public class HelperTests Assert.DoesNotContain("data-name=\"${v.name}\"", adminJs, StringComparison.Ordinal); } - private class FakeEnv : IWebHostEnvironment - { - public string ApplicationName { get; set; } = ""; - public IFileProvider WebRootFileProvider { get; set; } = null!; - public string WebRootPath { get; set; } = ""; - public string EnvironmentName { get; set; } = ""; - public string ContentRootPath { get; set; } = ""; - public IFileProvider ContentRootFileProvider { get; set; } = null!; - } - private static ForwardedHeadersOptions BuildForwardedHeadersOptionsForTest(IConfiguration config) { var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).First(m => m.Name.Contains("BuildForwardedHeadersOptions")); diff --git a/GameList.Tests/Support/TestWebApplicationFactory.cs b/GameList.Tests/Support/TestWebApplicationFactory.cs index 5a4e1c3..e0655af 100644 --- a/GameList.Tests/Support/TestWebApplicationFactory.cs +++ b/GameList.Tests/Support/TestWebApplicationFactory.cs @@ -26,7 +26,7 @@ internal class TestWebApplicationFactory : WebApplicationFactory services.Remove(descriptor); } - _connection = new SqliteConnection("Data Source=:memory:;Cache=Shared"); + _connection = new SqliteConnection($"Data Source=file:tests-{Guid.NewGuid():N}?mode=memory&cache=shared"); _connection.Open(); services.AddDbContext(options => { options.UseSqlite(_connection); }); @@ -44,7 +44,6 @@ internal class TestWebApplicationFactory : WebApplicationFactory using var scope = host.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); db.Database.Migrate(); return host; diff --git a/IIS.md b/IIS.md index bad3291..fb2f536 100644 --- a/IIS.md +++ b/IIS.md @@ -8,6 +8,7 @@ ## Publish - From repo root: `dotnet publish -c Release -o publish` +- Before first start (and after every new migration): run `dotnet ef database update` from repo root against the target environment. - Copy `publish/` contents to site directory (keep `App_Data` writable by the app pool user). - Set environment variables in web.config or IIS config: - `ASPNETCORE_ENVIRONMENT=Production` diff --git a/Program.cs b/Program.cs index 97f6ab1..8748b25 100644 --- a/Program.cs +++ b/Program.cs @@ -146,7 +146,6 @@ var basePath = builder.Configuration["BasePath"]; if (!string.IsNullOrWhiteSpace(basePath)) { app.UsePathBase(basePath); - UpdateIndexMetaBase(app.Environment, basePath); } app.UseGlobalExceptionLogging(); @@ -154,13 +153,6 @@ app.UseAuthentication(); app.UseMiddleware(); app.UseAuthorization(); -// Ensure database and migrations are applied on startup -using (var scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} - app.UseDefaultFiles(); app.UseStaticFiles(); @@ -274,42 +266,4 @@ static Task WriteUnauthorizedChallengeAsync(HttpContext context) return context.Response.WriteAsJsonAsync(problem); } -static void UpdateIndexMetaBase(IWebHostEnvironment env, string basePath) -{ - try - { - var indexPath = Path.Combine(env.WebRootPath, "index.html"); - if (!File.Exists(indexPath)) - return; - - var text = File.ReadAllText(indexPath); - var marker = "name=\"app-base\""; - var contentKey = "content=\""; - var markerIndex = text.IndexOf(marker, StringComparison.OrdinalIgnoreCase); - if (markerIndex < 0) - return; - - var contentIndex = text.IndexOf(contentKey, markerIndex, StringComparison.OrdinalIgnoreCase); - if (contentIndex < 0) - return; - - var valueStart = contentIndex + contentKey.Length; - var valueEnd = text.IndexOf('"', valueStart); - if (valueEnd < 0) - return; - - var current = text[valueStart..valueEnd]; - var normalized = basePath.EndsWith('/') ? basePath.TrimEnd('/') : basePath; - if (current == normalized) - return; - - var updated = text[..valueStart] + normalized + text[valueEnd..]; - File.WriteAllText(indexPath, updated); - } - catch - { - // If we can't rewrite, continue; frontend can still be set manually. - } -} - public partial class Program; diff --git a/README.md b/README.md index 14f357f..fcb561c 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,13 @@ Pick'n'Play is a .NET 10 ASP.NET Core Minimal API app with a static HTML/CSS/JS 1. Restore and build: `dotnet build GameList.sln` -2. Run tests: +2. Apply DB migrations explicitly: + `dotnet ef database update` +3. Run tests: `dotnet test GameList.Tests/GameList.Tests.csproj` -3. Run locally: +4. Run locally: `dotnet run --project GameList.csproj` -4. Open: +5. Open: `http://localhost:5000` (or the URL shown by `dotnet run`) ## Frontend Tooling @@ -28,6 +30,7 @@ Pick'n'Play is a .NET 10 ASP.NET Core Minimal API app with a static HTML/CSS/JS - Core invariants are DB-enforced: single owner account and non-joker suggestion cap. - Gameplay phases: `Suggest`, `Vote`, `Results`. - Storage: SQLite database under `App_Data/gamelist.db`. +- Migrations are deployment-time operations (`dotnet ef database update`); app startup does not auto-migrate. - Security defaults: rate-limited auth/admin routes, baseline browser security headers, production HTTPS+HSTS enforcement. ## Module Ownership diff --git a/TESTS.md b/TESTS.md index 0daecb8..a7b0d48 100644 --- a/TESTS.md +++ b/TESTS.md @@ -87,7 +87,7 @@ stateDiagram-v2 - EndpointHelpers.IsValidImageUrl/IsValidHttpUrl: accepts empty, http/https; rejects others/invalid ext. - IsReachableImageAsync: with mocked Http responses covers head success, get fallback, redirect rejection, size guard, and private/reserved host range detection (IPv4/IPv6). - BuildLinkRoots/LinkedIdsFor/FindRootId: cover disjoint groups, chains, cycles guard (visited set), non-existent ids. -- UpdateIndexMetaBase (Program.cs): rewrites app-base meta when BasePath set; no change when matching/marker missing; safe exceptions swallowed. +- Program startup avoids runtime frontend file rewrites; BasePath remains purely configuration/deploy managed. - Global exception handler returns 500 with JSON body and logs error. - /health returns {status:"ok"}. - Security middleware tests validate response headers and rate-limiting behavior on auth/admin routes.