using System.Net; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using allstarr.Controllers; using allstarr.Models.Settings; using allstarr.Services.Admin; namespace allstarr.Tests; public class AdminAuthControllerTests { [Fact] public async Task Login_WithValidNonAdminJellyfinUser_CreatesSessionAndCookie() { HttpRequestMessage? capturedRequest = null; string? capturedBody = null; var handler = new DelegateHttpMessageHandler(async (request, _) => { capturedRequest = request; capturedBody = request.Content is null ? null : await request.Content.ReadAsStringAsync(); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(""" { "AccessToken":"token-123", "ServerId":"server-1", "User":{ "Id":"user-1", "Name":"josh", "Policy":{"IsAdministrator":false} } } """) }; }); var sessionService = new AdminAuthSessionService(); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["X-Forwarded-Proto"] = "https"; var controller = CreateController(handler, sessionService, httpContext); var result = await controller.Login(new AdminAuthController.LoginRequest { Username = " josh ", Password = "secret-pass" }); var ok = Assert.IsType(result); var payloadJson = JsonSerializer.Serialize(ok.Value); using var payload = JsonDocument.Parse(payloadJson); Assert.True(payload.RootElement.GetProperty("authenticated").GetBoolean()); Assert.Equal("user-1", payload.RootElement.GetProperty("user").GetProperty("id").GetString()); Assert.Equal("josh", payload.RootElement.GetProperty("user").GetProperty("name").GetString()); Assert.False(payload.RootElement.GetProperty("user").GetProperty("isAdministrator").GetBoolean()); Assert.NotNull(capturedRequest); Assert.Equal(HttpMethod.Post, capturedRequest!.Method); Assert.Equal("http://jellyfin.local/Users/AuthenticateByName", capturedRequest.RequestUri?.ToString()); Assert.Contains("X-Emby-Authorization", capturedRequest.Headers.Select(h => h.Key)); Assert.NotNull(capturedBody); Assert.Contains("\"Username\":\"josh\"", capturedBody!); Assert.Contains("\"Pw\":\"secret-pass\"", capturedBody!); var setCookies = httpContext.Response.Headers.SetCookie; Assert.Single(setCookies); var setCookieHeader = setCookies[0] ?? string.Empty; Assert.Contains($"{AdminAuthSessionService.SessionCookieName}=", setCookieHeader); Assert.Contains("httponly", setCookieHeader.ToLowerInvariant()); Assert.Contains("secure", setCookieHeader.ToLowerInvariant()); Assert.Contains("samesite=strict", setCookieHeader.ToLowerInvariant()); var sessionId = ExtractCookieValue(setCookieHeader); Assert.True(sessionService.TryGetValidSession(sessionId, out var session)); Assert.Equal("user-1", session.UserId); Assert.Equal("josh", session.UserName); Assert.False(session.IsAdministrator); } [Fact] public async Task Login_WithInvalidCredentials_ReturnsUnauthorized() { var handler = new DelegateHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized))); var sessionService = new AdminAuthSessionService(); var httpContext = new DefaultHttpContext(); var controller = CreateController(handler, sessionService, httpContext); var result = await controller.Login(new AdminAuthController.LoginRequest { Username = "josh", Password = "wrong" }); var unauthorized = Assert.IsType(result); Assert.Equal(StatusCodes.Status401Unauthorized, unauthorized.StatusCode); Assert.False(httpContext.Response.Headers.ContainsKey("Set-Cookie")); } [Fact] public void GetCurrentSession_WithUnknownCookie_ReturnsUnauthenticated() { var handler = new DelegateHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); var sessionService = new AdminAuthSessionService(); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Cookie = $"{AdminAuthSessionService.SessionCookieName}=missing-session"; var controller = CreateController(handler, sessionService, httpContext); var result = controller.GetCurrentSession(); var ok = Assert.IsType(result); var payloadJson = JsonSerializer.Serialize(ok.Value); using var payload = JsonDocument.Parse(payloadJson); Assert.False(payload.RootElement.GetProperty("authenticated").GetBoolean()); var setCookies = httpContext.Response.Headers.SetCookie; Assert.Single(setCookies); var setCookieHeader = setCookies[0] ?? string.Empty; Assert.Contains($"{AdminAuthSessionService.SessionCookieName}=", setCookieHeader); Assert.Contains("expires=", setCookieHeader.ToLowerInvariant()); } [Fact] public void GetCurrentSession_WithValidCookie_ReturnsSessionUser() { var handler = new DelegateHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); var sessionService = new AdminAuthSessionService(); var session = sessionService.CreateSession( userId: "user-42", userName: "alice", isAdministrator: true, jellyfinAccessToken: "token", jellyfinServerId: "server"); var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Cookie = $"{AdminAuthSessionService.SessionCookieName}={session.SessionId}"; var controller = CreateController(handler, sessionService, httpContext); var result = controller.GetCurrentSession(); var ok = Assert.IsType(result); var payloadJson = JsonSerializer.Serialize(ok.Value); using var payload = JsonDocument.Parse(payloadJson); Assert.True(payload.RootElement.GetProperty("authenticated").GetBoolean()); Assert.Equal("user-42", payload.RootElement.GetProperty("user").GetProperty("id").GetString()); Assert.Equal("alice", payload.RootElement.GetProperty("user").GetProperty("name").GetString()); Assert.True(payload.RootElement.GetProperty("user").GetProperty("isAdministrator").GetBoolean()); } private static AdminAuthController CreateController( HttpMessageHandler handler, AdminAuthSessionService sessionService, HttpContext httpContext) { var jellyfinOptions = Options.Create(new JellyfinSettings { Url = "http://jellyfin.local" }); var httpClientFactory = new Mock(); httpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(new HttpClient(handler)); var logger = new Mock>(); var controller = new AdminAuthController( jellyfinOptions, httpClientFactory.Object, sessionService, logger.Object) { ControllerContext = new ControllerContext { HttpContext = httpContext } }; return controller; } private static string ExtractCookieValue(string setCookieHeader) { var cookiePart = setCookieHeader.Split(';', 2)[0]; var parts = cookiePart.Split('=', 2); return parts.Length == 2 ? parts[1] : string.Empty; } private sealed class DelegateHttpMessageHandler : HttpMessageHandler { private readonly Func> _handler; public DelegateHttpMessageHandler(Func> handler) { _handler = handler; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return _handler(request, cancellationToken); } } }