mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
feat: replace OpenSSL with native Blowfish decryption and add library rescan
- Replace OpenSSL subprocess with BouncyCastle native Blowfish CBC decryption - Add automatic Subsonic library scan trigger after downloads (with 30s debounce) - Improve error handling in DeezerMetadataService search methods - Add comprehensive tests for download service, metadata service, and library service - Increase test coverage from 13 to 32 tests
This commit is contained in:
165
octo-fiesta.Tests/DeezerDownloadServiceTests.cs
Normal file
165
octo-fiesta.Tests/DeezerDownloadServiceTests.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using octo_fiesta.Services;
|
||||
using octo_fiesta.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace octo_fiesta.Tests;
|
||||
|
||||
public class DeezerDownloadServiceTests : IDisposable
|
||||
{
|
||||
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
||||
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
||||
private readonly Mock<ILocalLibraryService> _localLibraryServiceMock;
|
||||
private readonly Mock<IMusicMetadataService> _metadataServiceMock;
|
||||
private readonly Mock<ILogger<DeezerDownloadService>> _loggerMock;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly string _testDownloadPath;
|
||||
|
||||
public DeezerDownloadServiceTests()
|
||||
{
|
||||
_testDownloadPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-download-tests-" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(_testDownloadPath);
|
||||
|
||||
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
||||
var httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
||||
|
||||
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
_localLibraryServiceMock = new Mock<ILocalLibraryService>();
|
||||
_metadataServiceMock = new Mock<IMusicMetadataService>();
|
||||
_loggerMock = new Mock<ILogger<DeezerDownloadService>>();
|
||||
|
||||
_configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Library:DownloadPath"] = _testDownloadPath,
|
||||
["Deezer:Arl"] = null,
|
||||
["Deezer:ArlFallback"] = null
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDownloadPath))
|
||||
{
|
||||
Directory.Delete(_testDownloadPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
private DeezerDownloadService CreateService(string? arl = null)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Library:DownloadPath"] = _testDownloadPath,
|
||||
["Deezer:Arl"] = arl,
|
||||
["Deezer:ArlFallback"] = null
|
||||
})
|
||||
.Build();
|
||||
|
||||
return new DeezerDownloadService(
|
||||
_httpClientFactoryMock.Object,
|
||||
config,
|
||||
_localLibraryServiceMock.Object,
|
||||
_metadataServiceMock.Object,
|
||||
_loggerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAvailableAsync_WithoutArl_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService(arl: null);
|
||||
|
||||
// Act
|
||||
var result = await service.IsAvailableAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAvailableAsync_WithEmptyArl_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService(arl: "");
|
||||
|
||||
// Act
|
||||
var result = await service.IsAvailableAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownloadSongAsync_WithUnsupportedProvider_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService(arl: "test-arl");
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotSupportedException>(() =>
|
||||
service.DownloadSongAsync("spotify", "123456"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownloadSongAsync_WhenAlreadyDownloaded_ReturnsExistingPath()
|
||||
{
|
||||
// Arrange
|
||||
var existingPath = Path.Combine(_testDownloadPath, "existing-song.mp3");
|
||||
await File.WriteAllTextAsync(existingPath, "fake audio content");
|
||||
|
||||
_localLibraryServiceMock
|
||||
.Setup(s => s.GetLocalPathForExternalSongAsync("deezer", "123456"))
|
||||
.ReturnsAsync(existingPath);
|
||||
|
||||
var service = CreateService(arl: "test-arl");
|
||||
|
||||
// Act
|
||||
var result = await service.DownloadSongAsync("deezer", "123456");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(existingPath, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDownloadStatus_WithUnknownSongId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService(arl: "test-arl");
|
||||
|
||||
// Act
|
||||
var result = service.GetDownloadStatus("unknown-id");
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownloadSongAsync_WhenSongNotFound_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
_localLibraryServiceMock
|
||||
.Setup(s => s.GetLocalPathForExternalSongAsync("deezer", "999999"))
|
||||
.ReturnsAsync((string?)null);
|
||||
|
||||
_metadataServiceMock
|
||||
.Setup(s => s.GetSongAsync("deezer", "999999"))
|
||||
.ReturnsAsync((Song?)null);
|
||||
|
||||
var service = CreateService(arl: "test-arl");
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<Exception>(() =>
|
||||
service.DownloadSongAsync("deezer", "999999"));
|
||||
|
||||
Assert.Equal("Song not found", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,97 @@ public class DeezerMetadataServiceTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchSongsAsync_WithEmptyResponse_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
SetupHttpResponse(JsonSerializer.Serialize(new { data = Array.Empty<object>() }));
|
||||
|
||||
// Act
|
||||
var result = await _service.SearchSongsAsync("nonexistent", 20);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchSongsAsync_WithHttpError_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
SetupHttpResponse("Error", HttpStatusCode.InternalServerError);
|
||||
|
||||
// Act
|
||||
var result = await _service.SearchSongsAsync("test", 20);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAlbumAsync_WithDeezerProvider_ReturnsAlbumWithTracks()
|
||||
{
|
||||
// Arrange
|
||||
var deezerResponse = new
|
||||
{
|
||||
id = 456789,
|
||||
title = "Test Album",
|
||||
nb_tracks = 2,
|
||||
release_date = "2023-05-20",
|
||||
cover_medium = "https://example.com/album.jpg",
|
||||
artist = new { id = 123, name = "Test Artist" },
|
||||
tracks = new
|
||||
{
|
||||
data = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = 111,
|
||||
title = "Track 1",
|
||||
duration = 180,
|
||||
track_position = 1,
|
||||
artist = new { id = 123, name = "Test Artist" },
|
||||
album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" }
|
||||
},
|
||||
new
|
||||
{
|
||||
id = 222,
|
||||
title = "Track 2",
|
||||
duration = 200,
|
||||
track_position = 2,
|
||||
artist = new { id = 123, name = "Test Artist" },
|
||||
album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAlbumAsync("deezer", "456789");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("ext-deezer-456789", result.Id);
|
||||
Assert.Equal("Test Album", result.Title);
|
||||
Assert.Equal("Test Artist", result.Artist);
|
||||
Assert.Equal(2, result.Songs.Count);
|
||||
Assert.Equal("Track 1", result.Songs[0].Title);
|
||||
Assert.Equal("Track 2", result.Songs[1].Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAlbumAsync_WithNonDeezerProvider_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetAlbumAsync("spotify", "123456");
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private void SetupHttpResponse(string content, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
_httpMessageHandlerMock
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using octo_fiesta.Services;
|
||||
using octo_fiesta.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using System.Net;
|
||||
|
||||
namespace octo_fiesta.Tests;
|
||||
|
||||
@@ -8,6 +13,7 @@ public class LocalLibraryServiceTests : IDisposable
|
||||
{
|
||||
private readonly LocalLibraryService _service;
|
||||
private readonly string _testDownloadPath;
|
||||
private readonly Mock<IHttpClientFactory> _mockHttpClientFactory;
|
||||
|
||||
public LocalLibraryServiceTests()
|
||||
{
|
||||
@@ -21,7 +27,25 @@ public class LocalLibraryServiceTests : IDisposable
|
||||
})
|
||||
.Build();
|
||||
|
||||
_service = new LocalLibraryService(configuration);
|
||||
// Mock HttpClient
|
||||
var mockHandler = new Mock<HttpMessageHandler>();
|
||||
mockHandler.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("{\"subsonic-response\":{\"status\":\"ok\",\"scanStatus\":{\"scanning\":false,\"count\":100}}}")
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(mockHandler.Object);
|
||||
_mockHttpClientFactory = new Mock<IHttpClientFactory>();
|
||||
_mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
var subsonicSettings = Options.Create(new SubsonicSettings { Url = "http://localhost:4533" });
|
||||
var mockLogger = new Mock<ILogger<LocalLibraryService>>();
|
||||
|
||||
_service = new LocalLibraryService(configuration, _mockHttpClientFactory.Object, subsonicSettings, mockLogger.Object);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -152,4 +176,45 @@ public class LocalLibraryServiceTests : IDisposable
|
||||
// Assert - nothing to assert, just checking it doesn't throw
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerLibraryScanAsync_ReturnsTrue()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.TriggerLibraryScanAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetScanStatusAsync_ReturnsScanStatus()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetScanStatusAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.Scanning);
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ext-deezer-123", true, "deezer", "123")]
|
||||
[InlineData("ext-spotify-abc123", true, "spotify", "abc123")]
|
||||
[InlineData("ext-tidal-999-888", true, "tidal", "999-888")]
|
||||
[InlineData("123456", false, null, null)]
|
||||
[InlineData("", false, null, null)]
|
||||
[InlineData("ext-", false, null, null)]
|
||||
[InlineData("ext-deezer", false, null, null)]
|
||||
public void ParseSongId_VariousInputs_ReturnsExpected(string songId, bool expectedIsExternal, string? expectedProvider, string? expectedExternalId)
|
||||
{
|
||||
// Act
|
||||
var (isExternal, provider, externalId) = _service.ParseSongId(songId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedIsExternal, isExternal);
|
||||
Assert.Equal(expectedProvider, provider);
|
||||
Assert.Equal(expectedExternalId, externalId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user