Files
allstarr/allstarr.Tests/Providers/MultiProviderDownloadServiceTests.cs
T

188 lines
7.1 KiB
C#

using allstarr.Models.Download;
using allstarr.Models.Settings;
using allstarr.Services;
using allstarr.Services.Common;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
namespace allstarr.Tests;
public sealed class MultiProviderDownloadServiceTests
{
[Theory]
[InlineData("qobuz", "qobuz", "42", "https://open.qobuz.com/track/42")]
[InlineData("spotiflac-deezer", "deezer", "3135556", "https://www.deezer.com/track/3135556")]
[InlineData("spotiflac-apple-music", "apple-music", "2037093408", "https://music.apple.com/us/song/2037093408")]
public void OdesliTrackUrl_UsesProviderTrackRoutes(string provider, string normalized, string trackId, string expected)
{
Assert.Equal(normalized, OdesliService.NormalizeProviderId(provider));
Assert.Equal(expected, OdesliService.BuildTrackUrl(provider, trackId));
}
[Fact]
public async Task StreamingUsesAccountFreeAffinityAndDeniesAccountRequiredFallback()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["MULTI_PROVIDER_STREAMING_ORDER"] = "apple-download,deezer",
["MULTI_PROVIDER_DOWNLOAD_ORDER"] = "apple-download,deezer"
})
.Build();
var clients = new HttpFactory();
var status = new ProviderStatusManager(
configuration,
clients,
NullLogger<ProviderStatusManager>.Instance,
Options.Create(new SpotifyApiSettings()),
Options.Create(new AppleDownloadSettings { BaseUrl = "http://apple-gateway" }),
Options.Create(new DeezerSettings { Arl = "configured-arl" }),
Options.Create(new QobuzSettings()));
var apple = new PrimaryRecordingService();
var deezer = new SecondaryRecordingService();
var metadata = new Mock<IMusicMetadataService>();
var service = new MultiProviderDownloadService(
[apple, deezer],
[],
metadata.Object,
status,
new OdesliService(
clients,
NullLogger<OdesliService>.Instance,
Mock.Of<IApplicationCache>()),
NullLogger<MultiProviderDownloadService>.Instance);
await using var stream = await service.DownloadAndStreamAsync("apple-download", "track-1");
Assert.Equal(("apple-download", "track-1"), apple.Call);
metadata.Verify(
item => item.GetSongAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()),
Times.Never);
await Assert.ThrowsAsync<NotSupportedException>(() =>
service.DownloadAndStreamAsync("deezer", "track-2"));
Assert.Null(deezer.Call);
metadata.Verify(
item => item.GetSongAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task StreamingPropagatesClientCancellation()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["MULTI_PROVIDER_STREAMING_ORDER"] = "apple-download"
})
.Build();
var clients = new HttpFactory();
var status = new ProviderStatusManager(
configuration,
clients,
NullLogger<ProviderStatusManager>.Instance,
Options.Create(new SpotifyApiSettings()),
Options.Create(new AppleDownloadSettings { BaseUrl = "http://apple-gateway" }),
Options.Create(new DeezerSettings()),
Options.Create(new QobuzSettings()));
var service = new MultiProviderDownloadService(
[new AppleMusicCancelingService()],
[],
Mock.Of<IMusicMetadataService>(),
status,
new OdesliService(
clients,
NullLogger<OdesliService>.Instance,
Mock.Of<IApplicationCache>()),
NullLogger<MultiProviderDownloadService>.Instance);
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
service.DownloadAndStreamAsync(
"apple-download",
"track-1",
cancellationToken: cancellation.Token));
}
private sealed class HttpFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name) => new();
}
private sealed class AppleMusicCancelingService : IConcreteDownloadService
{
public string ProviderId => "apple-download";
public Task<string> DownloadSongAsync(
string externalProvider,
string externalId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<Stream> DownloadAndStreamAsync(
string externalProvider,
string externalId,
StreamQuality? qualityOverride = null,
CancellationToken cancellationToken = default) =>
Task.FromCanceled<Stream>(cancellationToken);
public DownloadInfo? GetDownloadStatus(string songId) => null;
public IReadOnlyList<DownloadInfo> GetActiveDownloads() => [];
public Task<string?> GetLocalPathIfExistsAsync(string externalProvider, string externalId) =>
Task.FromResult<string?>(null);
public Task<bool> IsAvailableAsync() => Task.FromResult(true);
}
private abstract class RecordingService : IConcreteDownloadService
{
public abstract string ProviderId { get; }
public (string Provider, string Id)? Call { get; private set; }
public Task<string> DownloadSongAsync(
string externalProvider,
string externalId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<Stream> DownloadAndStreamAsync(
string externalProvider,
string externalId,
StreamQuality? qualityOverride = null,
CancellationToken cancellationToken = default)
{
Call = (externalProvider, externalId);
return Task.FromResult<Stream>(new MemoryStream([1]));
}
public DownloadInfo? GetDownloadStatus(string songId) => null;
public IReadOnlyList<DownloadInfo> GetActiveDownloads() => [];
public Task<string?> GetLocalPathIfExistsAsync(string externalProvider, string externalId) =>
Task.FromResult<string?>(null);
public Task<bool> IsAvailableAsync() => Task.FromResult(true);
}
private sealed class PrimaryRecordingService : RecordingService
{
public override string ProviderId => "apple-download";
}
private sealed class SecondaryRecordingService : RecordingService
{
public override string ProviderId => "deezer";
}
}