mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-06 21:55:39 -04:00
614 lines
26 KiB
C#
614 lines
26 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Net;
|
|
using System.Text;
|
|
using allstarr.Models.Settings;
|
|
using allstarr.Services.Common;
|
|
using allstarr.Services.AppleMusic;
|
|
using allstarr.Core.Capabilities;
|
|
using allstarr.Core.Operations;
|
|
using Moq;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public sealed class ProviderStatusManagerTests
|
|
{
|
|
[Fact]
|
|
public async Task ActiveExtensionCapabilitiesReceiveReadinessChecks()
|
|
{
|
|
var implementation = new Mock<IProviderMetadataCapability>();
|
|
implementation.SetupGet(item => item.ProviderId).Returns("fixture-extension");
|
|
implementation.SetupGet(item => item.Capability).Returns(ProviderCapabilityKind.Metadata);
|
|
var registry = new ProviderRegistry([new ProviderRegistration(new ProviderDescriptor(
|
|
"fixture-extension", "Fixture", "Fixture extension", ProviderOrigin.Extension, "1", "1.0",
|
|
[new(ProviderCapabilityKind.Metadata, ProviderCapabilitySupportState.Supported,
|
|
ProviderAccountRequirement.None, "1.0", ["searchTracks", "getTrack"], [])],
|
|
new ProviderPermissionDescriptor(), entryPoint: "index.js"), [implementation.Object])]);
|
|
var manager = CreateManager(new Dictionary<string, string?>(), providerRegistry: registry);
|
|
|
|
var before = Assert.Single(manager.GetAllAccountFreeStatuses(),
|
|
item => item.Provider == "fixture-extension" && item.Capability == "metadata");
|
|
var after = await manager.TestAccountFreeProviderCapabilityAsync("fixture-extension", "metadata");
|
|
|
|
Assert.True(manager.CanTestCapability("fixture-extension", "metadata"));
|
|
Assert.False(before.IsReady);
|
|
Assert.True(after.IsReady);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpotifyLyricsSidecar_DoesNotRequireDirectSpotifyApiMode()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_LYRICS_ORDER"] = "spotify,lrclib"
|
|
},
|
|
spotifySettings: new SpotifyApiSettings
|
|
{
|
|
Enabled = false,
|
|
SessionCookie = string.Empty,
|
|
LyricsApiUrl = "http://lyrics-sidecar:8080"
|
|
});
|
|
|
|
var status = manager.GetAccountFreeStatus("spotify", ProviderCapabilities.Lyrics);
|
|
|
|
Assert.Equal(ProviderConfigurationState.Configured, status.Configuration);
|
|
Assert.Contains("spotify", manager.GetEnabledLyricsProviders());
|
|
}
|
|
|
|
[Fact]
|
|
public void DisabledProvider_IsRemovedFromEveryCurrentCapabilityLane()
|
|
{
|
|
var manager = CreateManager(new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_METADATA_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_ENABLED_SEARCH"] = "deezer",
|
|
["MULTI_PROVIDER_DOWNLOAD_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_STREAMING_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_PLAYLIST_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_ENABLED_PLAYLIST"] = "deezer",
|
|
["MULTI_PROVIDER_LYRICS_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_DISABLED_PROVIDERS"] = "DeEzEr"
|
|
});
|
|
|
|
Assert.Empty(manager.GetEnabledSearchProviders());
|
|
Assert.Empty(manager.GetEnabledDownloadProviders());
|
|
Assert.Empty(manager.GetEnabledStreamingProviders());
|
|
Assert.Empty(manager.GetEnabledPlaylistProviders());
|
|
Assert.Empty(manager.GetEnabledLyricsProviders());
|
|
}
|
|
|
|
[Fact]
|
|
public void UnknownProvider_IsNeverAdvertisedWhenOrdersContainIt()
|
|
{
|
|
var manager = CreateManager(new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_METADATA_ORDER"] = "unknown-provider",
|
|
["MULTI_PROVIDER_ENABLED_SEARCH"] = "unknown-provider",
|
|
["MULTI_PROVIDER_DOWNLOAD_ORDER"] = "unknown-provider",
|
|
["MULTI_PROVIDER_STREAMING_ORDER"] = "unknown-provider",
|
|
["MULTI_PROVIDER_PLAYLIST_ORDER"] = "unknown-provider",
|
|
["MULTI_PROVIDER_ENABLED_PLAYLIST"] = "unknown-provider"
|
|
});
|
|
|
|
Assert.Empty(manager.GetEnabledSearchProviders());
|
|
Assert.Empty(manager.GetEnabledDownloadProviders());
|
|
Assert.Empty(manager.GetEnabledStreamingProviders());
|
|
Assert.Empty(manager.GetEnabledPlaylistProviders());
|
|
}
|
|
|
|
[Fact]
|
|
public void PlaybackProviders_PreserveStreamingThenDownloadOrderWithoutDuplicates()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_STREAMING_ORDER"] = "deezer,apple-download",
|
|
["MULTI_PROVIDER_DOWNLOAD_ORDER"] = "apple-download,deezer"
|
|
},
|
|
appleMusicSettings: new AppleDownloadSettings { BaseUrl = "http://apple-gateway" },
|
|
deezerSettings: new DeezerSettings { Arl = "configured-arl" });
|
|
|
|
Assert.Equal(["apple-download"], manager.GetEnabledPlaybackProviders());
|
|
}
|
|
|
|
[Fact]
|
|
public void ManagedStatusRead_IsPureAndDoesNotInventHealthOrTestTime()
|
|
{
|
|
var factory = new CountingHttpClientFactory();
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: factory);
|
|
|
|
var status = manager.GetManagedStatus(
|
|
"DeEzEr",
|
|
"MeTaDaTa",
|
|
Guid.CreateVersion7(),
|
|
new Dictionary<string, string>());
|
|
|
|
Assert.Equal("deezer", status.Provider);
|
|
Assert.Equal(ProviderCapabilities.Metadata, status.Capability);
|
|
Assert.Equal(ProviderConfigurationState.NotRequired, status.Configuration);
|
|
Assert.Equal(ProviderHealthState.Unknown, status.Health);
|
|
Assert.Null(status.TestedAt);
|
|
Assert.True(status.CanAttempt);
|
|
Assert.False(status.IsReady);
|
|
Assert.Equal(0, factory.CreateCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingManagedDeezerSecret_DoesNotBlockPublicMetadata()
|
|
{
|
|
var manager = CreateManager(new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_METADATA_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_ENABLED_SEARCH"] = "deezer",
|
|
["MULTI_PROVIDER_DOWNLOAD_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_STREAMING_ORDER"] = "deezer"
|
|
});
|
|
|
|
Assert.Equal(["deezer"], manager.GetEnabledSearchProviders());
|
|
Assert.Empty(manager.GetEnabledDownloadProviders());
|
|
Assert.Empty(manager.GetEnabledStreamingProviders());
|
|
|
|
var metadata = manager.GetAccountFreeStatus("deezer", ProviderCapabilities.Metadata);
|
|
var download = manager.GetManagedStatus(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
Guid.CreateVersion7(),
|
|
new Dictionary<string, string>());
|
|
Assert.Equal(ProviderConfigurationState.NotRequired, metadata.Configuration);
|
|
Assert.True(metadata.CanAttempt);
|
|
Assert.Equal(ProviderConfigurationState.NeedsConfiguration, download.Configuration);
|
|
Assert.Equal("missing_provider_account_secret", download.ReasonCode);
|
|
Assert.False(download.CanAttempt);
|
|
}
|
|
|
|
[Fact]
|
|
public void AccountFreeProjection_ExcludesAccountRequiredCapabilities()
|
|
{
|
|
var statuses = CreateManager(new Dictionary<string, string?>()).GetAllAccountFreeStatuses();
|
|
|
|
Assert.Contains(statuses, item => item.Provider == "deezer" && item.Capability == ProviderCapabilities.Metadata);
|
|
Assert.Contains(statuses, item => item.Provider == "qobuz" && item.Capability == ProviderCapabilities.Metadata);
|
|
Assert.Contains(statuses, item => item.Provider == "apple-download" && item.Capability == ProviderCapabilities.Download);
|
|
Assert.DoesNotContain(statuses, item => item.Capability is ProviderCapabilities.Playlist or ProviderCapabilities.Scrobbling);
|
|
Assert.DoesNotContain(statuses, item =>
|
|
(item.Provider is "deezer" or "qobuz") &&
|
|
(item.Capability is ProviderCapabilities.Streaming or ProviderCapabilities.Download));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExplicitProbe_ExposesTestingThenHealthy()
|
|
{
|
|
var handler = new BlockingHandler();
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(handler));
|
|
|
|
var accountId = Guid.CreateVersion7();
|
|
var probe = manager.TestManagedProviderCapabilityAsync(
|
|
"deezer",
|
|
ProviderCapabilities.Metadata,
|
|
accountId,
|
|
new Dictionary<string, string>());
|
|
await handler.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
var testing = manager.GetManagedStatus(
|
|
"deezer", ProviderCapabilities.Metadata, accountId, new Dictionary<string, string>());
|
|
Assert.Equal(ProviderHealthState.Testing, testing.Health);
|
|
Assert.Null(testing.TestedAt);
|
|
Assert.False(testing.IsReady);
|
|
|
|
handler.Release.TrySetResult(true);
|
|
var completed = await probe;
|
|
|
|
Assert.Equal(ProviderHealthState.Healthy, completed.Health);
|
|
Assert.NotNull(completed.TestedAt);
|
|
Assert.True(completed.IsReady);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProbeObservations_AreIsolatedByProviderAccount()
|
|
{
|
|
var handler = new QueuedResponseHandler(
|
|
Json(HttpStatusCode.OK, "{\"id\":3135556}"),
|
|
Json(HttpStatusCode.ServiceUnavailable, "{}"));
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(handler));
|
|
|
|
var accountA = Guid.CreateVersion7();
|
|
var accountB = Guid.CreateVersion7();
|
|
var first = await manager.TestManagedProviderCapabilityAsync(
|
|
"deezer",
|
|
ProviderCapabilities.Metadata,
|
|
accountA,
|
|
new Dictionary<string, string>());
|
|
var second = await manager.TestManagedProviderCapabilityAsync(
|
|
"deezer",
|
|
ProviderCapabilities.Metadata,
|
|
accountB,
|
|
new Dictionary<string, string>());
|
|
|
|
Assert.Equal(ProviderHealthState.Healthy, first.Health);
|
|
Assert.Equal(ProviderHealthState.Degraded, second.Health);
|
|
Assert.Equal(
|
|
ProviderHealthState.Healthy,
|
|
manager.GetManagedStatus("deezer", ProviderCapabilities.Metadata, accountA, new Dictionary<string, string>()).Health);
|
|
Assert.Equal(
|
|
ProviderHealthState.Degraded,
|
|
manager.GetManagedStatus("deezer", ProviderCapabilities.Metadata, accountB, new Dictionary<string, string>()).Health);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FailedDownloadProbe_DoesNotDegradeDeezerMetadata()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_METADATA_ORDER"] = "deezer",
|
|
["MULTI_PROVIDER_ENABLED_SEARCH"] = "deezer",
|
|
["MULTI_PROVIDER_DOWNLOAD_ORDER"] = "deezer"
|
|
},
|
|
httpClientFactory: new HandlerHttpClientFactory(
|
|
new QueuedResponseHandler(Json(HttpStatusCode.Unauthorized, "{}"))),
|
|
deezerSettings: new DeezerSettings { Arl = "configured-arl" });
|
|
|
|
var download = await manager.TestManagedProviderCapabilityAsync(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
Guid.CreateVersion7(),
|
|
new Dictionary<string, string> { ["arl"] = "configured-arl" });
|
|
|
|
Assert.Equal(ProviderHealthState.Degraded, download.Health);
|
|
Assert.Empty(manager.GetEnabledDownloadProviders());
|
|
Assert.Equal(["deezer"], manager.GetEnabledSearchProviders());
|
|
|
|
var metadata = manager.GetAccountFreeStatus("deezer", ProviderCapabilities.Metadata);
|
|
Assert.Equal(ProviderHealthState.Unknown, metadata.Health);
|
|
Assert.True(metadata.CanAttempt);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeploymentCredentials_DoNotEnableAccountRequiredCapabilities()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
deezerSettings: new DeezerSettings { Arl = "deployment-arl" },
|
|
qobuzSettings: new QobuzSettings
|
|
{
|
|
UserAuthToken = "deployment-token",
|
|
UserId = "deployment-user"
|
|
});
|
|
|
|
Assert.Empty(manager.GetEnabledDownloadProviders());
|
|
Assert.False(manager.CanTestAccountFreeCapability("deezer", ProviderCapabilities.Download));
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
manager.GetAccountFreeStatus("deezer", ProviderCapabilities.Download));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ManagedAccount_DoesNotBorrowAnotherGlobalCredential()
|
|
{
|
|
var http = new CountingHttpClientFactory();
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: http,
|
|
deezerSettings: new DeezerSettings { Arl = "deployment-global-arl" });
|
|
var accountId = Guid.CreateVersion7();
|
|
|
|
var status = manager.GetManagedStatus(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
accountId,
|
|
new Dictionary<string, string>());
|
|
var tested = await manager.TestManagedProviderCapabilityAsync(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
accountId,
|
|
new Dictionary<string, string>());
|
|
|
|
Assert.Equal(ProviderConfigurationState.NeedsConfiguration, status.Configuration);
|
|
Assert.Equal("missing_provider_account_secret", status.ReasonCode);
|
|
Assert.Equal(ProviderConfigurationState.NeedsConfiguration, tested.Configuration);
|
|
Assert.Equal(0, http.CreateCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ManagedLastFmAccount_UsesItsEncryptedAccountFieldsForProbe()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(
|
|
new QueuedResponseHandler(Json(HttpStatusCode.OK, "{\"user\":{\"name\":\"listener\"}}"))));
|
|
var accountId = Guid.CreateVersion7();
|
|
var secrets = new Dictionary<string, string>
|
|
{
|
|
["apikey"] = "api-key",
|
|
["sharedsecret"] = "shared-secret",
|
|
["sessionkey"] = "session-key"
|
|
};
|
|
|
|
var status = manager.GetManagedStatus("lastfm", ProviderCapabilities.Scrobbling, accountId, secrets);
|
|
var tested = await manager.TestManagedProviderCapabilityAsync(
|
|
"lastfm", ProviderCapabilities.Scrobbling, accountId, secrets);
|
|
|
|
Assert.Equal(ProviderConfigurationState.Configured, status.Configuration);
|
|
Assert.Equal(ProviderHealthState.Healthy, tested.Health);
|
|
Assert.True(tested.IsReady);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ManagedListenBrainzAccount_ValidatesItsOwnToken()
|
|
{
|
|
var handler = new QueuedResponseHandler(
|
|
Json(HttpStatusCode.OK, "{\"valid\":true,\"user_name\":\"listener\"}"));
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(handler));
|
|
var accountId = Guid.CreateVersion7();
|
|
var secrets = new Dictionary<string, string>
|
|
{
|
|
["token"] = "user-token",
|
|
["baseUrl"] = "https://koito.example/apis/listenbrainz/1"
|
|
};
|
|
|
|
var tested = await manager.TestManagedProviderCapabilityAsync(
|
|
"listenbrainz", ProviderCapabilities.Scrobbling, accountId, secrets);
|
|
|
|
Assert.Equal(ProviderHealthState.Healthy, tested.Health);
|
|
Assert.True(tested.IsReady);
|
|
Assert.Equal("https://koito.example/apis/listenbrainz/1/validate-token",
|
|
Assert.Single(handler.Requests).AbsoluteUri);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SpotifyAccount_ProbesPlaylistAndLyricsSidecar()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(
|
|
new QueuedResponseHandler(
|
|
SpotifyTotpSecrets(),
|
|
SpotifyTime(),
|
|
Json(HttpStatusCode.OK, "{\"accessToken\":\"access-token\"}"),
|
|
Json(HttpStatusCode.OK, "{\"error\":false,\"syncType\":\"LINE_SYNCED\",\"lines\":[]}"))),
|
|
spotifySettings: new SpotifyApiSettings
|
|
{
|
|
Enabled = true,
|
|
SessionCookie = "deployment-cookie",
|
|
LyricsApiUrl = "http://lyrics-sidecar:8080"
|
|
});
|
|
var accountId = Guid.CreateVersion7();
|
|
var secrets = new Dictionary<string, string> { ["sessioncookie"] = "account-cookie" };
|
|
|
|
Assert.True(manager.CanTestCapability("spotify", ProviderCapabilities.Lyrics));
|
|
Assert.True(await manager.TestManagedProviderConnectionAsync("spotify", accountId, secrets));
|
|
Assert.Equal(
|
|
ProviderHealthState.Healthy,
|
|
manager.GetManagedStatus("spotify", ProviderCapabilities.Playlist, accountId, secrets).Health);
|
|
Assert.Equal(
|
|
ProviderHealthState.Healthy,
|
|
manager.GetManagedStatus("spotify", ProviderCapabilities.Lyrics, accountId, secrets).Health);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SpotifyPlaylistProbe_ReportsUnauthorizedWithoutExposingCookie()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(
|
|
new QueuedResponseHandler(SpotifyTotpSecrets(), SpotifyTime(), Json(HttpStatusCode.Unauthorized, "{}"))),
|
|
spotifySettings: new SpotifyApiSettings { Enabled = true, SessionCookie = "expired-cookie" });
|
|
|
|
var result = await manager.TestManagedProviderCapabilityAsync(
|
|
"spotify",
|
|
ProviderCapabilities.Playlist,
|
|
Guid.CreateVersion7(),
|
|
new Dictionary<string, string> { ["sessioncookie"] = "expired-cookie" });
|
|
|
|
Assert.Equal(ProviderHealthState.Degraded, result.Health);
|
|
Assert.Equal("provider_unauthorized", result.ReasonCode);
|
|
Assert.DoesNotContain("expired-cookie", result.ReasonCode, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SpotifyPlaylistProbe_ReportsTotpTokenEndpointForbidden()
|
|
{
|
|
var blocked = new HttpResponseMessage(HttpStatusCode.Forbidden)
|
|
{
|
|
Content = new StringContent("<html>blocked by edge</html>", Encoding.UTF8, "text/html")
|
|
};
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(new QueuedResponseHandler(SpotifyTotpSecrets(), SpotifyTime(), blocked)),
|
|
spotifySettings: new SpotifyApiSettings { Enabled = true, SessionCookie = "valid-looking-cookie" });
|
|
|
|
var result = await manager.TestManagedProviderCapabilityAsync(
|
|
"spotify",
|
|
ProviderCapabilities.Playlist,
|
|
Guid.CreateVersion7(),
|
|
new Dictionary<string, string> { ["sessioncookie"] = "valid-looking-cookie" });
|
|
|
|
Assert.Equal(ProviderHealthState.Degraded, result.Health);
|
|
Assert.Equal("provider_forbidden", result.ReasonCode);
|
|
Assert.DoesNotContain("valid-looking-cookie", result.ReasonCode, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ManagedAccountStatus_PreservesCapabilityFailureReason()
|
|
{
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>(),
|
|
httpClientFactory: new HandlerHttpClientFactory(
|
|
new QueuedResponseHandler(Json(HttpStatusCode.Unauthorized, "{}"))));
|
|
var accountId = Guid.CreateVersion7();
|
|
var secrets = new Dictionary<string, string> { ["arl"] = "account-arl" };
|
|
|
|
var tested = await manager.TestManagedProviderCapabilityAsync(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
accountId,
|
|
secrets);
|
|
var current = manager.GetManagedStatus(
|
|
"deezer",
|
|
ProviderCapabilities.Download,
|
|
accountId,
|
|
secrets);
|
|
|
|
Assert.Equal(ProviderHealthState.Degraded, tested.Health);
|
|
Assert.Equal(ProviderConfigurationState.Configured, current.Configuration);
|
|
Assert.Equal(ProviderHealthState.Degraded, current.Health);
|
|
Assert.Equal("probe_failed", current.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExpiredAccountFreeFailure_RemainsExcludedUntilReprobed()
|
|
{
|
|
var clock = new FakeClock(new DateTimeOffset(2026, 8, 24, 12, 0, 0, TimeSpan.Zero));
|
|
var discovery = new Mock<IAppleDownloadEndpointDiscovery>();
|
|
discovery.Setup(item => item.DiscoverAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new AppleDownloadEndpointSnapshot(
|
|
AppleDownloadEndpointState.Unreachable,
|
|
"endpoint_unreachable",
|
|
null,
|
|
false,
|
|
[new AppleDownloadCapabilityStatus(
|
|
ProviderCapabilities.Metadata,
|
|
AppleDownloadCapabilityState.Degraded,
|
|
"endpoint_unreachable")]));
|
|
var manager = CreateManager(
|
|
new Dictionary<string, string?>
|
|
{
|
|
["MULTI_PROVIDER_METADATA_ORDER"] = "apple-download,deezer",
|
|
["MULTI_PROVIDER_ENABLED_SEARCH"] = "apple-download,deezer"
|
|
},
|
|
appleMusicSettings: new AppleDownloadSettings { BaseUrl = "http://apple-gateway" },
|
|
appleDownloadDiscovery: discovery.Object,
|
|
clock: clock);
|
|
|
|
var failed = await manager.TestAccountFreeProviderCapabilityAsync(
|
|
"apple-download",
|
|
ProviderCapabilities.Metadata);
|
|
clock.UtcNow += TimeSpan.FromHours(1);
|
|
|
|
Assert.Equal(ProviderHealthState.Degraded, failed.Health);
|
|
Assert.Equal(["deezer"], manager.GetEnabledSearchProviders());
|
|
Assert.Equal(
|
|
ProviderHealthState.Degraded,
|
|
manager.GetAccountFreeStatus("apple-download", ProviderCapabilities.Metadata).Health);
|
|
}
|
|
|
|
private static ProviderStatusManager CreateManager(
|
|
IReadOnlyDictionary<string, string?> values,
|
|
IHttpClientFactory? httpClientFactory = null,
|
|
SpotifyApiSettings? spotifySettings = null,
|
|
AppleDownloadSettings? appleMusicSettings = null,
|
|
DeezerSettings? deezerSettings = null,
|
|
QobuzSettings? qobuzSettings = null,
|
|
IProviderRegistry? providerRegistry = null,
|
|
IAppleDownloadEndpointDiscovery? appleDownloadDiscovery = null,
|
|
IPlatformClock? clock = null)
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(values)
|
|
.Build();
|
|
|
|
return new ProviderStatusManager(
|
|
configuration,
|
|
httpClientFactory ?? new CountingHttpClientFactory(),
|
|
NullLogger<ProviderStatusManager>.Instance,
|
|
Options.Create(spotifySettings ?? new SpotifyApiSettings()),
|
|
Options.Create(appleMusicSettings ?? new AppleDownloadSettings()),
|
|
Options.Create(deezerSettings ?? new DeezerSettings()),
|
|
Options.Create(qobuzSettings ?? new QobuzSettings()),
|
|
appleDownloadDiscovery: appleDownloadDiscovery,
|
|
services: providerRegistry == null
|
|
? null
|
|
: Mock.Of<IServiceProvider>(provider =>
|
|
provider.GetService(typeof(IProviderRegistry)) == providerRegistry),
|
|
clock: clock);
|
|
}
|
|
|
|
private static HttpResponseMessage Json(HttpStatusCode status, string json) =>
|
|
new(status)
|
|
{
|
|
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
|
};
|
|
|
|
private static HttpResponseMessage SpotifyTotpSecrets() => Json(
|
|
HttpStatusCode.OK,
|
|
System.Text.Json.JsonSerializer.Serialize(new[]
|
|
{
|
|
new { version = 1, secret = Enumerable.Range(1, 32).ToArray() }
|
|
}));
|
|
|
|
private static HttpResponseMessage SpotifyTime()
|
|
{
|
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
|
response.Headers.Date = DateTimeOffset.UtcNow;
|
|
return response;
|
|
}
|
|
|
|
private sealed class CountingHttpClientFactory : IHttpClientFactory
|
|
{
|
|
public int CreateCount { get; private set; }
|
|
|
|
public HttpClient CreateClient(string name)
|
|
{
|
|
CreateCount++;
|
|
throw new InvalidOperationException("A status read must not create an HTTP client.");
|
|
}
|
|
}
|
|
|
|
private sealed class HandlerHttpClientFactory(HttpMessageHandler handler) : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
|
|
}
|
|
|
|
private sealed class BlockingHandler : HttpMessageHandler
|
|
{
|
|
public TaskCompletionSource<bool> Started { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public TaskCompletionSource<bool> Release { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Started.TrySetResult(true);
|
|
await Release.Task.WaitAsync(cancellationToken);
|
|
return Json(HttpStatusCode.OK, "{\"id\":3135556}");
|
|
}
|
|
}
|
|
|
|
private sealed class QueuedResponseHandler(params HttpResponseMessage[] responses) : HttpMessageHandler
|
|
{
|
|
private readonly ConcurrentQueue<HttpResponseMessage> _responses = new(responses);
|
|
public List<Uri> Requests { get; } = [];
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Requests.Add(request.RequestUri!);
|
|
if (!_responses.TryDequeue(out var response))
|
|
{
|
|
throw new InvalidOperationException("No fake provider response remains.");
|
|
}
|
|
|
|
return Task.FromResult(response);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeClock(DateTimeOffset utcNow) : IPlatformClock
|
|
{
|
|
public DateTimeOffset UtcNow { get; set; } = utcNow;
|
|
}
|
|
}
|