mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 14:05:02 -04:00
626 lines
26 KiB
C#
626 lines
26 KiB
C#
using System.Net;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
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 Microsoft.Extensions.DependencyInjection;
|
|
using Moq;
|
|
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 allstarr.Core.Storage;
|
|
using allstarr.Core.Health;
|
|
using allstarr.Core.Operations;
|
|
using allstarr.Core.Secrets;
|
|
using allstarr.Core.Settings;
|
|
using allstarr.Core.Configuration;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public class ConfigControllerAuthorizationTests : IAsyncLifetime
|
|
{
|
|
private readonly string _root = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"allstarr-tests",
|
|
Guid.NewGuid().ToString("N"));
|
|
private readonly Guid _providerAccountId = Guid.CreateVersion7();
|
|
private readonly Guid _tenantId = Guid.CreateVersion7();
|
|
private readonly Guid _userId = Guid.CreateVersion7();
|
|
private PostgresTestDatabase _database = null!;
|
|
private TestDbContextFactory _factory = null!;
|
|
private DurableStorageState _storageState = null!;
|
|
private readonly string _keyRingPath;
|
|
|
|
public ConfigControllerAuthorizationTests()
|
|
{
|
|
Directory.CreateDirectory(_root);
|
|
_keyRingPath = Path.Combine(_root, "keyring.json");
|
|
WriteKeyRing();
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_database = await PostgresTestDatabase.CreateAsync();
|
|
_factory = new TestDbContextFactory(_database.Options);
|
|
_storageState = new DurableStorageState(new DurableStorageOptions
|
|
{
|
|
Provider = "Postgres",
|
|
ConnectionString = _database.ConnectionString
|
|
});
|
|
_storageState.Set(DurableStorageReadiness.Ready, "fixture");
|
|
await using var context = await _factory.CreateDbContextAsync();
|
|
await context.Database.MigrateAsync();
|
|
context.Tenants.Add(new TenantRecord
|
|
{
|
|
Id = _tenantId,
|
|
Slug = "config-test",
|
|
Name = "Config test",
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
context.Users.Add(new PlatformUserRecord
|
|
{
|
|
Id = _userId,
|
|
TenantId = _tenantId,
|
|
DisplayName = "Test administrator",
|
|
Status = PlatformUserStatus.Active,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
context.ProviderAccounts.Add(new ProviderAccountRecord
|
|
{
|
|
Id = _providerAccountId,
|
|
ProviderId = "deezer",
|
|
DisplayName = "Health fixture",
|
|
Scope = ProviderAccountScope.Global,
|
|
Enabled = true,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
await context.SaveChangesAsync();
|
|
}
|
|
[Fact]
|
|
public async Task UpdateConfig_WithoutAdminSession_ReturnsForbidden()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
var result = await controller.UpdateConfig(new ConfigUpdateRequest
|
|
{
|
|
Updates = new Dictionary<string, string> { ["TEST_KEY"] = "value" }
|
|
});
|
|
|
|
AssertForbidden(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RestartContainer_WithoutAdminSession_ReturnsForbidden()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
var result = await controller.RestartContainer();
|
|
|
|
AssertForbidden(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExportEnv_WithoutAdminSession_ReturnsForbidden()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
var result = controller.ExportEnv();
|
|
|
|
AssertForbidden(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImportEnv_WithoutAdminSession_ReturnsForbidden()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
var file = new FormFile(Stream.Null, 0, 0, "file", "config.env");
|
|
var result = await controller.ImportEnv(file);
|
|
|
|
AssertForbidden(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateConfig_WithAdminSession_ContinuesToValidation()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
var result = await controller.UpdateConfig(new ConfigUpdateRequest());
|
|
|
|
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
|
Assert.Equal(StatusCodes.Status400BadRequest, badRequest.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateConfig_WritesDurableSettingsWithoutModifyingEnvFile()
|
|
{
|
|
var envPath = Path.Combine(_root, ".env");
|
|
await File.WriteAllTextAsync(envPath, "CACHE_LYRICS_DAYS=14\n");
|
|
var cache = new TestMemoryApplicationCache();
|
|
await cache.SetStringAsync("lyrics:v2:fixture", "cached");
|
|
var controller = CreateController(
|
|
CreateHttpContextWithSession(isAdmin: true),
|
|
applicationCache: cache);
|
|
|
|
var result = Assert.IsType<OkObjectResult>(await controller.UpdateConfig(new ConfigUpdateRequest
|
|
{
|
|
Updates = new Dictionary<string, string> { ["CACHE_LYRICS_DAYS"] = "45" }
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
|
Assert.Equal("CACHE_LYRICS_DAYS=14\n", await File.ReadAllTextAsync(envPath));
|
|
await using var db = await _factory.CreateDbContextAsync();
|
|
var setting = Assert.Single(await db.TenantRuntimeSettings.ToListAsync());
|
|
Assert.Equal(_tenantId, setting.TenantId);
|
|
Assert.Equal("Cache:LyricsDays", setting.Key);
|
|
Assert.Equal("45", setting.ValueJson);
|
|
Assert.False(await cache.ExistsAsync("lyrics:v2:fixture"));
|
|
|
|
var getResult = Assert.IsType<OkObjectResult>(await controller.GetConfig());
|
|
using var config = JsonDocument.Parse(JsonSerializer.Serialize(
|
|
getResult.Value,
|
|
new JsonSerializerOptions(JsonSerializerDefaults.Web)));
|
|
Assert.Equal(45, config.RootElement.GetProperty("cache").GetProperty("lyricsDays").GetInt32());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WholesaleImportEnv_IsRetiredForAdministrators()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
var result = Assert.IsType<ObjectResult>(await controller.ImportEnv(
|
|
new FormFile(Stream.Null, 0, 0, "file", "legacy.env")));
|
|
Assert.Equal(StatusCodes.Status410Gone, result.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateConfig_RejectsDeploymentKeysWithoutModifyingEnvFile()
|
|
{
|
|
var envPath = Path.Combine(_root, ".env");
|
|
await File.WriteAllTextAsync(envPath, "JELLYFIN_URL=http://old\n");
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
|
|
var result = Assert.IsType<BadRequestObjectResult>(await controller.UpdateConfig(new ConfigUpdateRequest
|
|
{
|
|
Updates = new Dictionary<string, string> { ["JELLYFIN_URL"] = "http://new" }
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
|
|
Assert.Equal("JELLYFIN_URL=http://old\n", await File.ReadAllTextAsync(envPath));
|
|
await using var db = await _factory.CreateDbContextAsync();
|
|
Assert.Empty(await db.TenantRuntimeSettings.ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MigrationEndpoints_RequireAdministratorSession()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
AssertForbidden(await controller.GetEnvMigrationStatus());
|
|
var bytes = Encoding.UTF8.GetBytes("CACHE_LYRICS_DAYS=30");
|
|
AssertForbidden(await controller.PreviewEnvMigration(
|
|
new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "legacy.env")));
|
|
AssertForbidden(await controller.ApplyEnvMigration(new ConfigController.ApplyLegacyEnvMigrationRequest
|
|
{
|
|
PreviewToken = "token",
|
|
Revision = "revision",
|
|
Confirmed = true
|
|
}));
|
|
AssertForbidden(await controller.ResetEnvMigration(
|
|
new ConfigController.ResetLegacyEnvMigrationRequest
|
|
{
|
|
PreviewToken = "token"
|
|
}));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectiveTransferEndpoints_RequireAdministratorAndBoundedArchive()
|
|
{
|
|
var transfer = new SelectiveStateTransferService(
|
|
_factory,
|
|
new DurableStorageOptions { ConnectionString = _database.ConnectionString },
|
|
_storageState);
|
|
var request = new SelectiveStateTransferControllerRequest
|
|
{
|
|
File = new FormFile(Stream.Null, 0, 0, "File", "state.zip")
|
|
};
|
|
|
|
var nonAdministrator = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
AssertForbidden(await nonAdministrator.ExportSelectiveState(null, transfer));
|
|
AssertForbidden(await nonAdministrator.PreviewSelectiveState(request, transfer));
|
|
AssertForbidden(await nonAdministrator.ImportSelectiveState(request, transfer));
|
|
|
|
var administrator = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
Assert.IsType<BadRequestObjectResult>(
|
|
await administrator.PreviewSelectiveState(request, transfer));
|
|
Assert.IsType<BadRequestObjectResult>(
|
|
await administrator.ImportSelectiveState(request, transfer));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MigrationController_ReturnsNonCachedAdminPreviewAndRequiresExplicitConfirmation()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
const string source = "CACHE_LYRICS_DAYS=33\nSPOTIFY_API_SESSION_COOKIE=controller-secret";
|
|
var bytes = Encoding.UTF8.GetBytes(source);
|
|
var previewResult = Assert.IsType<OkObjectResult>(await controller.PreviewEnvMigration(
|
|
new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "legacy.env")));
|
|
var previewJson = JsonSerializer.Serialize(
|
|
previewResult.Value,
|
|
new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
|
Assert.DoesNotContain("controller-secret", previewJson, StringComparison.Ordinal);
|
|
Assert.Equal("no-store", controller.Response.Headers.CacheControl);
|
|
Assert.Equal("no-cache", controller.Response.Headers.Pragma);
|
|
using var previewDocument = JsonDocument.Parse(previewJson);
|
|
var token = previewDocument.RootElement.GetProperty("previewToken").GetString();
|
|
var revision = previewDocument.RootElement.GetProperty("revision").GetString();
|
|
|
|
var unconfirmed = Assert.IsType<BadRequestObjectResult>(await controller.ApplyEnvMigration(
|
|
new ConfigController.ApplyLegacyEnvMigrationRequest
|
|
{
|
|
PreviewToken = token,
|
|
Revision = revision,
|
|
Confirmed = false
|
|
}));
|
|
Assert.Equal(StatusCodes.Status400BadRequest, unconfirmed.StatusCode);
|
|
|
|
var applied = Assert.IsType<OkObjectResult>(await controller.ApplyEnvMigration(
|
|
new ConfigController.ApplyLegacyEnvMigrationRequest
|
|
{
|
|
PreviewToken = token,
|
|
Revision = revision,
|
|
Confirmed = true
|
|
}));
|
|
Assert.Equal(StatusCodes.Status200OK, applied.StatusCode);
|
|
await using var db = await _factory.CreateDbContextAsync();
|
|
Assert.Contains(await db.TenantRuntimeSettings.ToListAsync(), item => item.Key == "Cache:LyricsDays");
|
|
var account = Assert.Single(await db.ProviderAccounts.Where(item => item.ProviderId == "spotify").ToListAsync());
|
|
Assert.False(account.Enabled);
|
|
Assert.NotNull(account.SecretReferenceId);
|
|
|
|
var status = Assert.IsType<OkObjectResult>(await controller.GetEnvMigrationStatus());
|
|
using var statusDocument = JsonDocument.Parse(JsonSerializer.Serialize(
|
|
status.Value,
|
|
new JsonSerializerOptions(JsonSerializerDefaults.Web)));
|
|
Assert.True(statusDocument.RootElement.GetProperty("completed").GetBoolean());
|
|
Assert.False(statusDocument.RootElement.GetProperty("firstRun").GetBoolean());
|
|
Assert.False(statusDocument.RootElement.GetProperty("sourcePresent").GetBoolean());
|
|
}
|
|
|
|
[Fact]
|
|
public void ExportEnv_WithAdminSession_WhenFeatureDisabled_ReturnsNotFound()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
var result = controller.ExportEnv();
|
|
|
|
var notFound = Assert.IsType<NotFoundObjectResult>(result);
|
|
Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetProvidersStatus_ReturnsExactAccountCapabilitySnapshotsWithNoInventedTimestamp()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
|
|
var result = Assert.IsType<OkObjectResult>(await controller.GetProvidersStatus());
|
|
var json = JsonSerializer.Serialize(result.Value);
|
|
using var document = JsonDocument.Parse(json);
|
|
var statuses = document.RootElement;
|
|
|
|
Assert.Equal(4, statuses.GetArrayLength());
|
|
Assert.All(statuses.EnumerateArray(), status =>
|
|
{
|
|
Assert.Equal(_providerAccountId, status.GetProperty("providerAccountId").GetGuid());
|
|
Assert.Equal("global", status.GetProperty("accountScope").GetString());
|
|
Assert.Equal(JsonValueKind.Null, status.GetProperty("testedAt").ValueKind);
|
|
Assert.Equal("unknown", status.GetProperty("health").GetString());
|
|
Assert.True(status.TryGetProperty("configuration", out _));
|
|
Assert.True(status.TryGetProperty("capability", out _));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProviderHealthEndpoints_RequireAdministratorSessionAndAllowPublicCapabilityProbe()
|
|
{
|
|
var nonAdministrator = CreateController(CreateHttpContextWithSession(isAdmin: false));
|
|
AssertForbidden(await nonAdministrator.GetProvidersStatus());
|
|
AssertForbidden(await nonAdministrator.TestProvider(
|
|
"deezer",
|
|
ProviderCapabilities.Metadata,
|
|
_providerAccountId));
|
|
|
|
var administrator = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
var publicProbe = Assert.IsType<OkObjectResult>(await administrator.TestProvider(
|
|
"deezer",
|
|
ProviderCapabilities.Metadata));
|
|
Assert.Equal(StatusCodes.Status200OK, publicProbe.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TestProvider_RejectsUnsupportedCapabilityWithoutProbing()
|
|
{
|
|
var controller = CreateController(CreateHttpContextWithSession(isAdmin: true));
|
|
|
|
var result = await controller.TestProvider(
|
|
"deezer",
|
|
"recommendation",
|
|
_providerAccountId);
|
|
|
|
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
|
Assert.Equal(StatusCodes.Status400BadRequest, badRequest.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ManagedDownloadProbe_UsesEncryptedAccountSecretAndPersistsExactCapability()
|
|
{
|
|
var secretStore = CreateSecretStore();
|
|
var secret = await secretStore.StoreAsync(
|
|
tenantId: null,
|
|
purpose: $"provider-account:deezer:{_providerAccountId:N}",
|
|
plaintext: Encoding.UTF8.GetBytes("{\"arl\":\"selected-account-arl\"}"));
|
|
await using (var context = await _factory.CreateDbContextAsync())
|
|
{
|
|
var account = await context.ProviderAccounts.SingleAsync(
|
|
item => item.Id == _providerAccountId);
|
|
account.SecretReferenceId = secret.Id;
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new CapturingHandler(new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(
|
|
"{\"results\":{\"USER\":{\"USER_ID\":42}}}",
|
|
Encoding.UTF8,
|
|
"application/json")
|
|
});
|
|
var healthStore = CreateHealthStore();
|
|
var controller = CreateController(
|
|
CreateHttpContextWithSession(isAdmin: true),
|
|
httpClientFactory: new HandlerHttpClientFactory(handler),
|
|
healthStore: healthStore,
|
|
secretStore: secretStore);
|
|
|
|
var result = Assert.IsType<OkObjectResult>(await controller.TestProvider(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
_providerAccountId));
|
|
using (var resultDocument = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)))
|
|
{
|
|
Assert.True(resultDocument.RootElement.GetProperty("success").GetBoolean());
|
|
Assert.Equal("healthy", resultDocument.RootElement.GetProperty("health").GetString());
|
|
Assert.Equal(_providerAccountId, resultDocument.RootElement.GetProperty("providerAccountId").GetGuid());
|
|
}
|
|
|
|
Assert.Equal("arl=selected-account-arl", handler.CookieHeader);
|
|
Assert.Equal(HttpMethod.Post, handler.Method);
|
|
Assert.Contains("deezer.getUserData", handler.RequestUri?.Query, StringComparison.Ordinal);
|
|
|
|
await using (var context = await _factory.CreateDbContextAsync())
|
|
{
|
|
var sample = await context.ProviderHealthSamples.SingleAsync();
|
|
Assert.Equal(_providerAccountId, sample.ProviderAccountId);
|
|
Assert.Equal(ProviderCapabilities.Download, sample.Capability);
|
|
Assert.Equal(allstarr.Core.Storage.ProviderHealthState.Healthy, sample.State);
|
|
var rollup = await context.ProviderHealthRollups.SingleAsync();
|
|
Assert.Equal(_providerAccountId, rollup.ProviderAccountId);
|
|
Assert.Equal(1, rollup.SuccessCount);
|
|
}
|
|
|
|
var restartedHealthStore = CreateHealthStore();
|
|
await restartedHealthStore.InitializeAsync();
|
|
var restartedController = CreateController(
|
|
CreateHttpContextWithSession(isAdmin: true),
|
|
healthStore: restartedHealthStore,
|
|
secretStore: secretStore);
|
|
var statusResult = Assert.IsType<OkObjectResult>(await restartedController.GetProvidersStatus());
|
|
using var statusDocument = JsonDocument.Parse(JsonSerializer.Serialize(statusResult.Value));
|
|
var download = statusDocument.RootElement.EnumerateArray().Single(item =>
|
|
item.GetProperty("providerAccountId").GetGuid() == _providerAccountId &&
|
|
item.GetProperty("capability").GetString() == ProviderCapabilities.Download);
|
|
Assert.Equal("configured", download.GetProperty("configuration").GetString());
|
|
Assert.Equal("healthy", download.GetProperty("health").GetString());
|
|
Assert.Equal(JsonValueKind.String, download.GetProperty("testedAt").ValueKind);
|
|
}
|
|
|
|
private 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,
|
|
TenantId = _tenantId,
|
|
AllstarrUserId = _userId
|
|
};
|
|
|
|
return context;
|
|
}
|
|
|
|
private ConfigController CreateController(
|
|
HttpContext httpContext,
|
|
Dictionary<string, string?>? configValues = null,
|
|
IHttpClientFactory? httpClientFactory = null,
|
|
DurableProviderHealthStore? healthStore = null,
|
|
EncryptedSecretStore? secretStore = null,
|
|
IApplicationCache? applicationCache = null)
|
|
{
|
|
var logger = new Mock<ILogger<ConfigController>>();
|
|
configValues ??= new Dictionary<string, string?>();
|
|
configValues.TryAdd("Backend:Type", "Jellyfin");
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(configValues)
|
|
.Build();
|
|
|
|
var webHostEnvironment = new Mock<IWebHostEnvironment>();
|
|
webHostEnvironment.SetupGet(e => e.EnvironmentName).Returns(Environments.Development);
|
|
var contentRoot = Path.Combine(_root, "app");
|
|
Directory.CreateDirectory(contentRoot);
|
|
webHostEnvironment.SetupGet(e => e.ContentRootPath).Returns(contentRoot);
|
|
applicationCache ??= new DisabledApplicationCache();
|
|
var spotifySessionCookieService = new SpotifySessionCookieService(
|
|
Options.Create(new SpotifyApiSettings()));
|
|
var providerStatusManager = new ProviderStatusManager(
|
|
configuration,
|
|
httpClientFactory ?? Mock.Of<IHttpClientFactory>(),
|
|
Mock.Of<ILogger<ProviderStatusManager>>(),
|
|
Options.Create(new SpotifyApiSettings()),
|
|
Options.Create(new AppleDownloadSettings()),
|
|
Options.Create(new DeezerSettings()),
|
|
Options.Create(new QobuzSettings()),
|
|
Options.Create(new SquidWTFSettings()),
|
|
new SquidWtfEndpointCatalog([], []),
|
|
extensionManager: null,
|
|
healthStore);
|
|
var effectiveSecretStore = secretStore ?? CreateSecretStore();
|
|
var clock = new SystemPlatformClock();
|
|
var signal = new RuntimeSettingsChangeSignal();
|
|
var durableSettings = new DurableRuntimeSettingsService(_factory, configuration, clock, signal);
|
|
var migration = new LegacyEnvMigrationService(_factory, durableSettings, effectiveSecretStore, clock);
|
|
var services = new ServiceCollection()
|
|
.AddSingleton(providerStatusManager)
|
|
.AddSingleton<IDbContextFactory<AllstarrDbContext>>(_factory)
|
|
.AddSingleton(durableSettings)
|
|
.AddSingleton<IDurableRuntimeSettings>(durableSettings)
|
|
.AddSingleton(migration)
|
|
.AddSingleton(effectiveSecretStore);
|
|
httpContext.RequestServices = services.BuildServiceProvider();
|
|
|
|
var controller = new ConfigController(
|
|
logger.Object,
|
|
configuration,
|
|
Options.Create(new SpotifyApiSettings()),
|
|
Options.Create(new JellyfinSettings()),
|
|
Options.Create(new SubsonicSettings()),
|
|
Options.Create(new DeezerSettings()),
|
|
Options.Create(new QobuzSettings()),
|
|
Options.Create(new SquidWTFSettings()),
|
|
Options.Create(new AppleDownloadSettings()),
|
|
Options.Create(new MusicBrainzSettings()),
|
|
Options.Create(new SpotifyImportSettings()),
|
|
Options.Create(new ScrobblingSettings()),
|
|
webHostEnvironment.Object,
|
|
spotifySessionCookieService,
|
|
applicationCache)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = httpContext
|
|
}
|
|
};
|
|
|
|
return controller;
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
if (_database is not null)
|
|
{
|
|
await _database.DisposeAsync();
|
|
}
|
|
|
|
if (Directory.Exists(_root))
|
|
{
|
|
Directory.Delete(_root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private DurableProviderHealthStore CreateHealthStore() => new(
|
|
_factory,
|
|
_storageState,
|
|
new ProviderHealthOptions
|
|
{
|
|
FailureThreshold = 3,
|
|
CircuitOpenSeconds = 30,
|
|
SampleTtlSeconds = 300,
|
|
RollupWindowMinutes = 15,
|
|
SampleRetentionDays = 7
|
|
},
|
|
new SystemPlatformClock());
|
|
|
|
private EncryptedSecretStore CreateSecretStore()
|
|
{
|
|
var options = new SecretStoreOptions { KeyRingPath = _keyRingPath };
|
|
return new EncryptedSecretStore(
|
|
_factory,
|
|
new FileSecretKeyRingProvider(options),
|
|
options,
|
|
new SystemPlatformClock());
|
|
}
|
|
|
|
private void WriteKeyRing()
|
|
{
|
|
var document = JsonSerializer.Serialize(new
|
|
{
|
|
activeKeyId = "fixture-key",
|
|
keys = new Dictionary<string, string>
|
|
{
|
|
["fixture-key"] = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
|
}
|
|
});
|
|
File.WriteAllText(_keyRingPath, document);
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
File.SetUnixFileMode(
|
|
_keyRingPath,
|
|
UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
|
}
|
|
}
|
|
|
|
private static void AssertForbidden(IActionResult result)
|
|
{
|
|
var forbidden = Assert.IsType<ObjectResult>(result);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
|
|
var payload = JsonSerializer.Serialize(forbidden.Value);
|
|
using var document = JsonDocument.Parse(payload);
|
|
Assert.Equal("Administrator permissions required", document.RootElement.GetProperty("error").GetString());
|
|
}
|
|
|
|
private sealed class TestDbContextFactory(DbContextOptions<AllstarrDbContext> options)
|
|
: IDbContextFactory<AllstarrDbContext>
|
|
{
|
|
public AllstarrDbContext CreateDbContext() => new(options);
|
|
|
|
public Task<AllstarrDbContext> CreateDbContextAsync(
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(new AllstarrDbContext(options));
|
|
}
|
|
|
|
private sealed class HandlerHttpClientFactory(HttpMessageHandler handler) : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
|
|
}
|
|
|
|
private sealed class CapturingHandler(HttpResponseMessage response) : HttpMessageHandler
|
|
{
|
|
public string? CookieHeader { get; private set; }
|
|
public HttpMethod? Method { get; private set; }
|
|
public Uri? RequestUri { get; private set; }
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
CookieHeader = request.Headers.TryGetValues("Cookie", out var cookies)
|
|
? Assert.Single(cookies)
|
|
: null;
|
|
Method = request.Method;
|
|
RequestUri = request.RequestUri;
|
|
return Task.FromResult(response);
|
|
}
|
|
}
|
|
}
|