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

193 lines
6.9 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using allstarr.Core.Capabilities;
using allstarr.Core.Protocols;
using allstarr.Models.Domain;
using allstarr.Models.Settings;
using allstarr.Services.Common;
using allstarr.Services.Local;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
namespace allstarr.Tests;
public sealed class ManagedTrackCacheServiceTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task CompletedFullRangeStream_IsPublishedAndRegistered(bool fallback)
{
var root = CreateRoot();
try
{
string? registeredPath = null;
var provider = fallback ? "qobuz" : "deezer";
var externalId = fallback ? "other-track" : "track-1";
var local = new Mock<ILocalLibraryService>(MockBehavior.Strict);
local.Setup(item => item.GetLocalPathForExternalSongAsync(provider, externalId))
.ReturnsAsync((string?)null);
local.Setup(item => item.RegisterDownloadedSongAsync(It.IsAny<Song>(), It.IsAny<string>()))
.Callback<Song, string>((song, path) =>
{
Assert.Equal(provider, song.ExternalProvider);
Assert.Equal(externalId, song.ExternalId);
registeredPath = path;
})
.Returns(Task.CompletedTask);
var service = CreateService(root, "Cache", local.Object);
using var response = Response(HttpStatusCode.PartialContent, [1, 2, 3, 4]);
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(0, 3, 4);
await service.WrapAsync(
ProviderStream(response) with { ServingProviderId = provider, ServingExternalId = externalId },
"deezer",
"track-1",
ProviderAudioQuality.Any,
headOnly: false,
() => Task.FromResult<Song?>(new Song
{
Title = "Track",
Artist = "Artist",
Album = "Album",
Track = 1
}),
CancellationToken.None);
Assert.Equal([1, 2, 3, 4], await response.Content.ReadAsByteArrayAsync());
Assert.NotNull(registeredPath);
Assert.True(File.Exists(registeredPath));
Assert.Equal([1, 2, 3, 4], await File.ReadAllBytesAsync(registeredPath));
local.VerifyAll();
}
finally
{
DeleteRoot(root);
}
}
[Fact]
public async Task InterruptedStream_DeletesPartialFileAndDoesNotRegister()
{
var root = CreateRoot();
try
{
var local = new Mock<ILocalLibraryService>(MockBehavior.Strict);
var service = CreateService(root, "Cache", local.Object);
using var response = Response(HttpStatusCode.OK, [1, 2, 3, 4]);
await service.WrapAsync(
ProviderStream(response),
"deezer",
"track-2",
ProviderAudioQuality.Any,
headOnly: false,
() => Task.FromResult<Song?>(null),
CancellationToken.None);
await using (var stream = await response.Content.ReadAsStreamAsync())
Assert.Equal(1, stream.ReadByte());
Assert.Empty(Directory.Exists(root)
? Directory.GetFiles(root, "*", SearchOption.AllDirectories)
: []);
local.Verify(item => item.RegisterDownloadedSongAsync(
It.IsAny<Song>(), It.IsAny<string>()), Times.Never);
}
finally
{
DeleteRoot(root);
}
}
[Fact]
public async Task PartialRangeOrPermanentMode_DoesNotCreateTrackCache()
{
foreach (var storageMode in new[] { "Cache", "Permanent" })
{
var root = CreateRoot();
try
{
var local = new Mock<ILocalLibraryService>(MockBehavior.Strict);
var service = CreateService(root, storageMode, local.Object);
using var response = Response(
storageMode == "Cache" ? HttpStatusCode.PartialContent : HttpStatusCode.OK,
[2, 3, 4]);
if (storageMode == "Cache")
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(1, 3, 4);
await service.WrapAsync(
ProviderStream(response),
"deezer",
"track-3",
ProviderAudioQuality.Any,
headOnly: false,
() => Task.FromResult<Song?>(null),
CancellationToken.None);
Assert.Equal([2, 3, 4], await response.Content.ReadAsByteArrayAsync());
Assert.False(Directory.Exists(Path.Combine(root, "cache")));
local.VerifyNoOtherCalls();
}
finally
{
DeleteRoot(root);
}
}
}
private static ManagedTrackCacheService CreateService(
string root,
string storageMode,
ILocalLibraryService local)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Library:DownloadPath"] = root
})
.Build();
return new ManagedTrackCacheService(
configuration,
Options.Create(new SubsonicSettings
{
StorageMode = Enum.Parse<StorageMode>(storageMode)
}),
local,
NullLogger<ManagedTrackCacheService>.Instance);
}
private static HttpResponseMessage Response(HttpStatusCode status, byte[] bytes)
{
var response = new HttpResponseMessage(status) { Content = new ByteArrayContent(bytes) };
response.Content.Headers.ContentType = new MediaTypeHeaderValue("audio/flac");
return response;
}
private static ProtocolProviderStream ProviderStream(HttpResponseMessage response) => new(
response,
new ProviderStreamLease(
"lease",
new Uri("https://media.example.test/track"),
DateTimeOffset.UtcNow.AddMinutes(1),
supportsByteRanges: true,
supportsSeeking: true,
new ProviderMediaFormat("audio/flac", "flac", "flac"),
ProviderStreamRetryBehavior.DoNotRetry),
"deezer");
private static string CreateRoot()
{
var root = Path.Combine(Path.GetTempPath(), "allstarr-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
return root;
}
private static void DeleteRoot(string root)
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}