Files
allstarr/allstarr.Tests/AdminAuthControllerTests.cs
T

214 lines
8.7 KiB
C#

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<OkObjectResult>(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<UnauthorizedObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<IHttpClientFactory>();
httpClientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(new HttpClient(handler));
var logger = new Mock<ILogger<AdminAuthController>>();
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<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handler;
public DelegateHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler)
{
_handler = handler;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _handler(request, cancellationToken);
}
}
}