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.