using System.Net; using System.Net.Http; using System.Text; using allstarr.Controllers; using allstarr.Models.Admin; using allstarr.Models.Settings; using allstarr.Services.Admin; using allstarr.Services.Common; using allstarr.Services.Spotify; using allstarr.Services.SquidWTF; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; namespace allstarr.Tests; public class DiagnosticsControllerTests { [Fact] public async Task TestSquidWtfEndpoints_WithoutAdministratorSession_ReturnsForbidden() { var controller = CreateController( CreateHttpContextWithSession(isAdmin: false), _ => new HttpResponseMessage(HttpStatusCode.OK)); var result = await controller.TestSquidWtfEndpoints(CancellationToken.None); var forbidden = Assert.IsType(result); Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode); } [Fact] public async Task TestSquidWtfEndpoints_ReturnsIndependentApiAndStreamingResults() { var controller = CreateController( CreateHttpContextWithSession(isAdmin: true), request => { var uri = request.RequestUri!; if (uri.Host == "node-one.example" && uri.AbsolutePath == "/search/") { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( """ {"data":{"items":[{"id":227242909,"title":"Monica Lewinsky"}]}} """, Encoding.UTF8, "application/json") }; } if (uri.Host == "node-one.example" && uri.AbsolutePath == "/track/") { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( """ {"data":{"manifest":"ZmFrZS1tYW5pZmVzdA=="}} """, Encoding.UTF8, "application/json") }; } if (uri.Host == "node-two.example" && uri.AbsolutePath == "/search/") { return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); } if (uri.Host == "node-two.example" && uri.AbsolutePath == "/track/") { return new HttpResponseMessage(HttpStatusCode.GatewayTimeout); } throw new InvalidOperationException($"Unexpected request URI: {uri}"); }); var result = await controller.TestSquidWtfEndpoints(CancellationToken.None); var ok = Assert.IsType(result); var payload = Assert.IsType(ok.Value); Assert.Equal(2, payload.TotalRows); var nodeOne = Assert.Single(payload.Endpoints, e => e.Host == "node-one.example"); Assert.True(nodeOne.Api.Configured); Assert.True(nodeOne.Api.IsUp); Assert.Equal("up", nodeOne.Api.State); Assert.Equal(200, nodeOne.Api.StatusCode); Assert.True(nodeOne.Streaming.Configured); Assert.True(nodeOne.Streaming.IsUp); Assert.Equal("up", nodeOne.Streaming.State); Assert.Equal(200, nodeOne.Streaming.StatusCode); var nodeTwo = Assert.Single(payload.Endpoints, e => e.Host == "node-two.example"); Assert.True(nodeTwo.Api.Configured); Assert.False(nodeTwo.Api.IsUp); Assert.Equal("down", nodeTwo.Api.State); Assert.Equal(503, nodeTwo.Api.StatusCode); Assert.True(nodeTwo.Streaming.Configured); Assert.False(nodeTwo.Streaming.IsUp); Assert.Equal("down", nodeTwo.Streaming.State); Assert.Equal(504, nodeTwo.Streaming.StatusCode); } private static HttpContext CreateHttpContextWithSession(bool isAdmin) { var context = new DefaultHttpContext(); context.Connection.LocalPort = 5275; context.Items[AdminAuthSessionService.HttpContextSessionItemKey] = new AdminAuthSession { SessionId = "session-id", UserId = "user-id", UserName = "user", IsAdministrator = isAdmin, JellyfinAccessToken = "token", JellyfinServerId = "server-id", ExpiresAtUtc = DateTime.UtcNow.AddHours(1), LastSeenUtc = DateTime.UtcNow }; return context; } private static DiagnosticsController CreateController( HttpContext httpContext, Func responseFactory) { var logger = new Mock>(); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary()) .Build(); var webHostEnvironment = new Mock(); webHostEnvironment.SetupGet(e => e.EnvironmentName).Returns(Environments.Development); webHostEnvironment.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory()); var helperLogger = new Mock>(); var helperService = new AdminHelperService( helperLogger.Object, Options.Create(new JellyfinSettings()), webHostEnvironment.Object); var spotifyCookieLogger = new Mock>(); var spotifySessionCookieService = new SpotifySessionCookieService( Options.Create(new SpotifyApiSettings()), helperService, spotifyCookieLogger.Object); var redisLogger = new Mock>(); var redisCache = new RedisCacheService( Options.Create(new RedisSettings { Enabled = false, ConnectionString = "localhost:6379" }), redisLogger.Object, new Microsoft.Extensions.Caching.Memory.MemoryCache( new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions())); var httpClientFactory = new Mock(); httpClientFactory.Setup(f => f.CreateClient(It.IsAny())) .Returns(() => new HttpClient(new StubHttpMessageHandler(responseFactory))); var controller = new DiagnosticsController( logger.Object, configuration, Options.Create(new SpotifyApiSettings()), Options.Create(new SpotifyImportSettings()), Options.Create(new JellyfinSettings()), Options.Create(new DeezerSettings()), Options.Create(new QobuzSettings()), Options.Create(new SquidWTFSettings()), spotifySessionCookieService, new SquidWtfEndpointCatalog( new List { "https://node-one.example", "https://node-two.example" }, new List { "https://node-one.example", "https://node-two.example" }), redisCache, httpClientFactory.Object) { ControllerContext = new ControllerContext { HttpContext = httpContext } }; return controller; } private sealed class StubHttpMessageHandler : HttpMessageHandler { private readonly Func _responseFactory; public StubHttpMessageHandler(Func responseFactory) { _responseFactory = responseFactory; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(_responseFactory(request)); } } }