207 lines
7.7 KiB
C#
207 lines
7.7 KiB
C#
using System.Net;
|
|
using System.Net.Http;
|
|
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.DependencyInjection;
|
|
using Microsoft.Extensions.FileProviders;
|
|
using System.Linq;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
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<ArgumentException>(() => 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, "<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, new object?[] { 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, new object?[] { env, "/pick" });
|
|
|
|
Assert.Equal("<html></html>", 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(Array.Empty<byte>());
|
|
resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
|
|
resp.Content.Headers.ContentLength = 100;
|
|
return resp;
|
|
}
|
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new ByteArrayContent(Array.Empty<byte>())
|
|
{
|
|
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(System.Text.Encoding.UTF8.GetBytes("not image"));
|
|
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(new[] { (1, (int?)null), (2, 1), (3, (int?)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<int, int?>
|
|
{
|
|
{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()
|
|
{
|
|
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<JsonElement>();
|
|
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!;
|
|
}
|
|
}
|