using System.Net; using System.Net.Http.Headers; using System.Reflection; using GameList.Infrastructure; using GameList.Endpoints; using GameList.Tests.Support; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.FileProviders; using System.Text.Json; using System.Net.Http.Json; namespace GameList.Tests; public class HelperTests { [Fact] public void PasswordHasher_roundtrip_and_empty_guard() { var (hash, salt) = PasswordHasher.HashPassword("secret"); Assert.True(PasswordHasher.Verify("secret", hash, salt)); Assert.False(PasswordHasher.Verify("other", hash, salt)); Assert.Throws(() => PasswordHasher.HashPassword("")); } [Fact] public void UpdateIndexMetaBase_rewrites_content_value() { 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)); } [Fact] public async Task IsReachableImageAsync_rejects_redirect_and_accepts_image() { Assert.True(await EndpointHelpers.IsReachableImageAsync(null, new StubHttpClientFactory(new StubHttpMessageHandler()))); Assert.False(await EndpointHelpers.IsReachableImageAsync("http://127.0.0.1/img.png", new StubHttpClientFactory(new StubHttpMessageHandler()))); } [Fact] public async Task IsReachableImageAsync_handles_head_success_redirect_and_size_guard() { var handler = new StubHttpMessageHandler(); handler.SetResponder(req => { if (req.Method == HttpMethod.Head) { var resp = new HttpResponseMessage(HttpStatusCode.OK); resp.Content = new ByteArrayContent([]); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); resp.Content.Headers.ContentLength = 100; return resp; } return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent([]) { Headers = { ContentType = new MediaTypeHeaderValue("image/png"), ContentLength = 100 } } }; }); var ok = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler); Assert.True(ok); handler.SetResponder(_ => { var resp = new HttpResponseMessage(HttpStatusCode.Redirect); resp.Headers.Location = new Uri("http://example.com/other"); return resp; }); var redirect = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler); Assert.False(redirect); handler.SetResponder(_ => { var resp = new HttpResponseMessage(HttpStatusCode.OK); resp.Content = new ByteArrayContent(new byte[10]); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); resp.Content.Headers.ContentLength = 6 * 1024 * 1024; // over 5 MB return resp; }); var tooLarge = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler); Assert.False(tooLarge); } [Fact] public async Task IsReachableImageAsync_rejects_non_image_content() { var handler = new StubHttpMessageHandler(); handler.SetResponder(_ => { var resp = new HttpResponseMessage(HttpStatusCode.OK); resp.Content = new ByteArrayContent("not image"u8.ToArray()); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); resp.Content.Headers.ContentLength = 9; return resp; }); var result = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler); Assert.False(result); } [Fact] public void Link_root_helpers_handle_groups() { var roots = EndpointHelpers.BuildLinkRoots([(1, null), (2, 1), (3, null)]); Assert.Equal(1, roots[1]); Assert.Equal(1, roots[2]); Assert.Equal(3, roots[3]); var linked = EndpointHelpers.LinkedIdsFor(2, roots); Assert.Contains(1, linked); Assert.Contains(2, linked); } [Fact] public void Url_validation_rules() { Assert.True(EndpointHelpers.IsValidImageUrl("https://x.com/img.png")); Assert.False(EndpointHelpers.IsValidImageUrl("ftp://x/img.png")); Assert.True(EndpointHelpers.IsValidHttpUrl("http://x")); Assert.False(EndpointHelpers.IsValidHttpUrl("file://x")); } [Fact] public void Find_root_handles_cycles() { var parentMap = new Dictionary { { 1, 2 }, { 2, 3 }, { 3, 1 } }; var root = EndpointHelpers.FindRootId(1, parentMap); Assert.Equal(3, root); // cycle breaks on revisit } [Fact] public async Task Global_exception_handler_returns_json_error() { await using var factory = new TestWebApplicationFactory().WithWebHostBuilder(builder => { builder.Configure(app => { app.UseGlobalExceptionLogging(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/boom", _ => throw new InvalidOperationException("boom")); }); }); }); var client = factory.CreateClient(); var resp = await client.GetAsync("/boom"); Assert.Equal(HttpStatusCode.InternalServerError, resp.StatusCode); var json = await resp.Content.ReadFromJsonAsync(); Assert.Equal("Unexpected server error", json.GetProperty("error").GetString()); } 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!; } }