using System.Net; using System.Net.Http.Json; using System.Text.Json; using GameList.Infrastructure; using GameList.Tests.Support; using Microsoft.EntityFrameworkCore; namespace GameList.Tests; public class AuthTests { [Fact] public async Task Register_trims_limits_and_sets_cookie_and_normalized_username() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); var response = await client.PostAsJsonAsync("/api/auth/register", new { Username = " MixedCaseUser ", Password = "Pass123!", DisplayName = " Display Name ", AdminKey = (string?)null }); response.EnsureSuccessStatusCode(); Assert.True(response.Headers.TryGetValues("Set-Cookie", out var cookies) && cookies.Any(c => c.Contains(PlayerIdentityExtensions.PlayerCookieName))); await factory.WithDbContextAsync(async db => { var player = await db.Players.AsNoTracking().SingleAsync(p => p.Username == "MixedCaseUser"); Assert.Equal("mixedcaseuser", player.NormalizedUsername); Assert.True(player.DisplayName!.Length <= 16); Assert.NotEqual(Array.Empty(), player.PasswordHash); Assert.NotEqual(Array.Empty(), player.PasswordSalt); }); } [Fact] public async Task Register_rejects_overlength_username_or_display_name() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); var tooLongUser = new string('u', 25); var userResp = await client.PostAsJsonAsync("/api/auth/register", new { Username = tooLongUser, Password = "Pass123!", DisplayName = "short" }); Assert.Equal(HttpStatusCode.BadRequest, userResp.StatusCode); var longDisplay = new string('d', 17); var displayResp = await client.PostAsJsonAsync("/api/auth/register", new { Username = "okuser", Password = "Pass123!", DisplayName = longDisplay }); Assert.Equal(HttpStatusCode.BadRequest, displayResp.StatusCode); } [Fact] public async Task Login_sets_last_login_and_fills_missing_display_name() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("loginfill"); await factory.WithDbContextAsync(async db => { var player = await db.Players.FirstAsync(); player.DisplayName = null; player.LastLoginAt = DateTimeOffset.UnixEpoch; await db.SaveChangesAsync(); }); var login = await client.LoginAsync("loginfill", "Pass123!"); login.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var player = await db.Players.AsNoTracking().SingleAsync(); Assert.NotEqual(DateTimeOffset.UnixEpoch, player.LastLoginAt); Assert.Equal("loginfill", player.DisplayName); }); } [Fact] public async Task Register_with_admin_key_sets_admin_flag() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); var response = await client.RegisterAsync("adminuser", admin: true); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadFromJsonAsync(); Assert.True(json.GetProperty("isAdmin").GetBoolean()); } [Fact] public async Task Register_duplicate_username_returns_conflict() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); var first = await client.RegisterAsync("duplicate"); first.EnsureSuccessStatusCode(); var second = await client.RegisterAsync("duplicate"); Assert.Equal(HttpStatusCode.Conflict, second.StatusCode); } [Fact] public async Task Login_with_wrong_password_returns_unauthorized() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("player1"); var login = await client.LoginAsync("player1", "wrongpass"); Assert.Equal(HttpStatusCode.Unauthorized, login.StatusCode); } [Fact] public async Task Register_validates_required_fields() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); var missing = await client.PostAsJsonAsync("/api/auth/register", new { Username = "", Password = "", DisplayName = "" }); Assert.Equal(HttpStatusCode.BadRequest, missing.StatusCode); var badKey = await client.PostAsJsonAsync("/api/auth/register", new { Username = "u", Password = "p", DisplayName = "d", AdminKey = "wrong" }); Assert.Equal(HttpStatusCode.BadRequest, badKey.StatusCode); } [Fact] public async Task Non_admin_cannot_access_admin_routes() { await using var factory = new TestWebApplicationFactory(); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("regular"); var resp = await player.GetAsync("/api/admin/vote-status"); Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode); var json = await resp.Content.ReadFromJsonAsync(); Assert.Equal("Unauthorized", json.GetProperty("title").GetString()); Assert.Equal("Unauthorized", json.GetProperty("detail").GetString()); Assert.Equal("Unauthorized", json.GetProperty("error").GetString()); } [Fact] public async Task Admin_can_access_admin_routes() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("adminuser", admin: true); var resp = await admin.GetAsync("/api/admin/vote-status"); resp.EnsureSuccessStatusCode(); } [Fact] public async Task Logout_clears_cookie() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("logoutme"); var resp = await client.PostAsync("/api/auth/logout", null); resp.EnsureSuccessStatusCode(); Assert.True(resp.Headers.TryGetValues("Set-Cookie", out var cookies) && cookies.Any(c => c.Contains("player"))); } }