Remove startup migration and runtime frontend rewrites
This commit is contained in:
@@ -9,7 +9,6 @@ using Microsoft.AspNetCore.HttpOverrides;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
@@ -28,34 +27,10 @@ public class HelperTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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());
|
var hasRewriteMethod = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Any(m => m.Name.Contains("UpdateIndexMetaBase", StringComparison.Ordinal));
|
||||||
Directory.CreateDirectory(webRoot);
|
Assert.False(hasRewriteMethod);
|
||||||
var index = Path.Combine(webRoot, "index.html");
|
|
||||||
File.WriteAllText(index, "<meta name=\"app-base\" content=\"\">");
|
|
||||||
|
|
||||||
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, "<html></html>");
|
|
||||||
|
|
||||||
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("<html></html>", File.ReadAllText(index));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -349,16 +324,6 @@ public class HelperTests
|
|||||||
Assert.DoesNotContain("data-name=\"${v.name}\"", adminJs, StringComparison.Ordinal);
|
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)
|
private static ForwardedHeadersOptions BuildForwardedHeadersOptionsForTest(IConfiguration config)
|
||||||
{
|
{
|
||||||
var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).First(m => m.Name.Contains("BuildForwardedHeadersOptions"));
|
var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).First(m => m.Name.Contains("BuildForwardedHeadersOptions"));
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ internal class TestWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
services.Remove(descriptor);
|
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();
|
_connection.Open();
|
||||||
|
|
||||||
services.AddDbContext<AppDbContext>(options => { options.UseSqlite(_connection); });
|
services.AddDbContext<AppDbContext>(options => { options.UseSqlite(_connection); });
|
||||||
@@ -44,7 +44,6 @@ internal class TestWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
|
|
||||||
using var scope = host.Services.CreateScope();
|
using var scope = host.Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
db.Database.EnsureCreated();
|
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
|
|
||||||
return host;
|
return host;
|
||||||
|
|||||||
1
IIS.md
1
IIS.md
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
## Publish
|
## Publish
|
||||||
- From repo root: `dotnet publish -c Release -o 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).
|
- Copy `publish/` contents to site directory (keep `App_Data` writable by the app pool user).
|
||||||
- Set environment variables in web.config or IIS config:
|
- Set environment variables in web.config or IIS config:
|
||||||
- `ASPNETCORE_ENVIRONMENT=Production`
|
- `ASPNETCORE_ENVIRONMENT=Production`
|
||||||
|
|||||||
46
Program.cs
46
Program.cs
@@ -146,7 +146,6 @@ var basePath = builder.Configuration["BasePath"];
|
|||||||
if (!string.IsNullOrWhiteSpace(basePath))
|
if (!string.IsNullOrWhiteSpace(basePath))
|
||||||
{
|
{
|
||||||
app.UsePathBase(basePath);
|
app.UsePathBase(basePath);
|
||||||
UpdateIndexMetaBase(app.Environment, basePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseGlobalExceptionLogging();
|
app.UseGlobalExceptionLogging();
|
||||||
@@ -154,13 +153,6 @@ app.UseAuthentication();
|
|||||||
app.UseMiddleware<EnsurePlayerExistsMiddleware>();
|
app.UseMiddleware<EnsurePlayerExistsMiddleware>();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
// Ensure database and migrations are applied on startup
|
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
||||||
db.Database.Migrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
@@ -274,42 +266,4 @@ static Task WriteUnauthorizedChallengeAsync(HttpContext context)
|
|||||||
return context.Response.WriteAsJsonAsync(problem);
|
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;
|
public partial class Program;
|
||||||
|
|||||||
@@ -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:
|
1. Restore and build:
|
||||||
`dotnet build GameList.sln`
|
`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`
|
`dotnet test GameList.Tests/GameList.Tests.csproj`
|
||||||
3. Run locally:
|
4. Run locally:
|
||||||
`dotnet run --project GameList.csproj`
|
`dotnet run --project GameList.csproj`
|
||||||
4. Open:
|
5. Open:
|
||||||
`http://localhost:5000` (or the URL shown by `dotnet run`)
|
`http://localhost:5000` (or the URL shown by `dotnet run`)
|
||||||
|
|
||||||
## Frontend Tooling
|
## 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.
|
- Core invariants are DB-enforced: single owner account and non-joker suggestion cap.
|
||||||
- Gameplay phases: `Suggest`, `Vote`, `Results`.
|
- Gameplay phases: `Suggest`, `Vote`, `Results`.
|
||||||
- Storage: SQLite database under `App_Data/gamelist.db`.
|
- 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.
|
- Security defaults: rate-limited auth/admin routes, baseline browser security headers, production HTTPS+HSTS enforcement.
|
||||||
|
|
||||||
## Module Ownership
|
## Module Ownership
|
||||||
|
|||||||
2
TESTS.md
2
TESTS.md
@@ -87,7 +87,7 @@ stateDiagram-v2
|
|||||||
- EndpointHelpers.IsValidImageUrl/IsValidHttpUrl: accepts empty, http/https; rejects others/invalid ext.
|
- 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).
|
- 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.
|
- 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.
|
- Global exception handler returns 500 with JSON body and logs error.
|
||||||
- /health returns {status:"ok"}.
|
- /health returns {status:"ok"}.
|
||||||
- Security middleware tests validate response headers and rate-limiting behavior on auth/admin routes.
|
- Security middleware tests validate response headers and rate-limiting behavior on auth/admin routes.
|
||||||
|
|||||||
Reference in New Issue
Block a user