Increase coverage with identity and filter tests
This commit is contained in:
115
GameList.Tests/FiltersTests.cs
Normal file
115
GameList.Tests/FiltersTests.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using GameList.Data;
|
||||
using GameList.Domain;
|
||||
using GameList.Infrastructure;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace GameList.Tests;
|
||||
|
||||
public class FiltersTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Admin_only_filter_blocks_non_admin()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("user");
|
||||
|
||||
var ctx = await BuildContextAsync(factory, isAdmin: false);
|
||||
var filter = new AdminOnlyFilter();
|
||||
var result = await filter.InvokeAsync(ctx, _ => ValueTask.FromResult<object?>(Results.Ok()));
|
||||
await AssertStatusAsync(result, StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Phase_requirement_allows_admin_override_when_enabled()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var ctx = await BuildContextAsync(factory, isAdmin: true, phase: Phase.Suggest);
|
||||
var filter = new PhaseRequirementFilter(Phase.Vote, allowAdminOverride: true);
|
||||
var called = false;
|
||||
var result = await filter.InvokeAsync(ctx, _ =>
|
||||
{
|
||||
called = true;
|
||||
return ValueTask.FromResult<object?>(Results.Ok());
|
||||
});
|
||||
|
||||
Assert.True(called);
|
||||
await AssertStatusAsync(result, StatusCodes.Status200OK);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Phase_or_joker_filter_blocks_without_joker_in_vote()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var ctx = await BuildContextAsync(factory, isAdmin: false, phase: Phase.Vote, hasJoker: false);
|
||||
var filter = new PhaseOrJokerFilter();
|
||||
var result = await filter.InvokeAsync(ctx, _ => ValueTask.FromResult<object?>(Results.Ok()));
|
||||
await AssertStatusAsync(result, StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
private static async Task<TestInvocationContext> BuildContextAsync(TestWebApplicationFactory factory, bool isAdmin, Phase phase = Phase.Vote, bool hasJoker = false)
|
||||
{
|
||||
var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var player = new Player
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = $"user-{Guid.NewGuid():N}",
|
||||
NormalizedUsername = $"user-{Guid.NewGuid():N}",
|
||||
PasswordHash = new byte[] { 1 },
|
||||
PasswordSalt = new byte[] { 1 },
|
||||
IsAdmin = isAdmin,
|
||||
CurrentPhase = phase,
|
||||
HasJoker = hasJoker,
|
||||
DisplayName = "User"
|
||||
};
|
||||
db.Players.Add(player);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var ctx = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = scope.ServiceProvider,
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, player.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, player.Username),
|
||||
new Claim(PlayerIdentityExtensions.AdminClaim, isAdmin ? "true" : "false")
|
||||
}, "cookie"))
|
||||
};
|
||||
|
||||
return new TestInvocationContext(ctx);
|
||||
}
|
||||
|
||||
private class TestInvocationContext : EndpointFilterInvocationContext
|
||||
{
|
||||
private readonly DefaultHttpContext _context;
|
||||
|
||||
public TestInvocationContext(DefaultHttpContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public override HttpContext HttpContext => _context;
|
||||
public override object?[] Arguments => Array.Empty<object?>();
|
||||
public override T GetArgument<T>(int index) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static async Task AssertStatusAsync(object? result, int statusCode)
|
||||
{
|
||||
var http = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = new ServiceCollection()
|
||||
.AddLogging()
|
||||
.BuildServiceProvider()
|
||||
};
|
||||
http.Response.Body = new MemoryStream();
|
||||
await ((IResult)result!).ExecuteAsync(http);
|
||||
Assert.Equal(statusCode, http.Response.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,26 @@ public class HelperTests
|
||||
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())));
|
||||
// Private host should be rejected before network call
|
||||
Assert.False(await EndpointHelpers.IsReachableImageAsync("http://127.0.0.1/img.png", new StubHttpClientFactory(new StubHttpMessageHandler())));
|
||||
}
|
||||
|
||||
@@ -69,6 +84,20 @@ public class HelperTests
|
||||
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
|
||||
}
|
||||
|
||||
private class FakeEnv : IWebHostEnvironment
|
||||
{
|
||||
public string ApplicationName { get; set; } = "";
|
||||
|
||||
69
GameList.Tests/IdentityTests.cs
Normal file
69
GameList.Tests/IdentityTests.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Security.Claims;
|
||||
using GameList.Domain;
|
||||
using GameList.Infrastructure;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace GameList.Tests;
|
||||
|
||||
public class IdentityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Sign_in_sets_claims_and_cookie()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var ctx = BuildAuthContext(factory.Services);
|
||||
|
||||
var player = new Player
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "claimuser",
|
||||
NormalizedUsername = "claimuser",
|
||||
PasswordHash = new byte[] { 1 },
|
||||
PasswordSalt = new byte[] { 1 },
|
||||
DisplayName = "Claim",
|
||||
IsAdmin = true
|
||||
};
|
||||
|
||||
await PlayerIdentityExtensions.SignInPlayerAsync(ctx, player);
|
||||
|
||||
var cookies = ctx.Response.Headers["Set-Cookie"];
|
||||
Assert.NotNull(cookies);
|
||||
Assert.Contains(cookies!, v => v.Contains(PlayerIdentityExtensions.PlayerCookieName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sign_out_clears_principal()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var ctx = BuildAuthContext(factory.Services);
|
||||
var player = new Player();
|
||||
await PlayerIdentityExtensions.SignInPlayerAsync(ctx, player);
|
||||
|
||||
await PlayerIdentityExtensions.SignOutPlayerAsync(ctx);
|
||||
|
||||
Assert.False(ctx.User.Identity?.IsAuthenticated ?? false);
|
||||
}
|
||||
|
||||
private static DefaultHttpContext BuildAuthContext(IServiceProvider services)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
serviceCollection.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.Name = PlayerIdentityExtensions.PlayerCookieName;
|
||||
});
|
||||
serviceCollection.AddLogging();
|
||||
var provider = serviceCollection.BuildServiceProvider();
|
||||
|
||||
return new DefaultHttpContext
|
||||
{
|
||||
RequestServices = provider,
|
||||
Response = { Body = new MemoryStream() }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,15 @@ public class MiddlewareTests
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
|
||||
Assert.Contains(resp.Headers, h => h.Key.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Existing_player_passes_through_middleware()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("live");
|
||||
|
||||
var resp = await client.GetAsync("/api/state");
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using GameList.Domain;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using GameList.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GameList.Tests;
|
||||
|
||||
@@ -102,4 +105,34 @@ public class StateTests
|
||||
var resp = await factory.CreateClient().GetFromJsonAsync<JsonElement>("/health");
|
||||
Assert.Equal("ok", resp.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPhase_aligns_to_results_when_open()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var player = new Player
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "phase",
|
||||
NormalizedUsername = "phase",
|
||||
PasswordHash = new byte[] { 1 },
|
||||
PasswordSalt = new byte[] { 1 },
|
||||
DisplayName = "phase",
|
||||
CurrentPhase = Phase.Vote
|
||||
};
|
||||
db.Players.Add(player);
|
||||
var state = await db.AppState.FirstAsync();
|
||||
state.ResultsOpen = true;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var playerId = await db.Players.Select(p => p.Id).FirstAsync();
|
||||
var phase = await GameList.Endpoints.EndpointHelpers.GetPhase(db, playerId);
|
||||
|
||||
Assert.Equal(Phase.Results, phase);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user