feat: playlist implementation

This commit is contained in:
V1ck3s
2026-01-14 23:18:27 +01:00
committed by Vickes
parent 2c5daeefed
commit ebe6e90f39
21 changed files with 2561 additions and 54 deletions

View File

@@ -24,6 +24,7 @@ public class DeezerDownloadServiceTests : IDisposable
private readonly Mock<ILocalLibraryService> _localLibraryServiceMock;
private readonly Mock<IMusicMetadataService> _metadataServiceMock;
private readonly Mock<ILogger<DeezerDownloadService>> _loggerMock;
private readonly Mock<IServiceProvider> _serviceProviderMock;
private readonly IConfiguration _configuration;
private readonly string _testDownloadPath;
@@ -41,6 +42,7 @@ public class DeezerDownloadServiceTests : IDisposable
_localLibraryServiceMock = new Mock<ILocalLibraryService>();
_metadataServiceMock = new Mock<IMusicMetadataService>();
_loggerMock = new Mock<ILogger<DeezerDownloadService>>();
_serviceProviderMock = new Mock<IServiceProvider>();
_configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
@@ -90,6 +92,7 @@ public class DeezerDownloadServiceTests : IDisposable
_metadataServiceMock.Object,
subsonicSettings,
deezerSettings,
_serviceProviderMock.Object,
_loggerMock.Object);
}

View File

@@ -580,4 +580,226 @@ public class DeezerMetadataServiceTests
}
#endregion
#region Playlist Tests
[Fact]
public async Task SearchPlaylistsAsync_ReturnsListOfPlaylists()
{
// Arrange
var deezerResponse = new
{
data = new[]
{
new
{
id = 12345,
title = "Chill Vibes",
nb_tracks = 50,
picture_medium = "https://example.com/playlist1.jpg",
user = new { name = "Test User" }
},
new
{
id = 67890,
title = "Workout Mix",
nb_tracks = 30,
picture_medium = "https://example.com/playlist2.jpg",
user = new { name = "Gym Buddy" }
}
}
};
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
// Act
var result = await _service.SearchPlaylistsAsync("chill");
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("Chill Vibes", result[0].Name);
Assert.Equal(50, result[0].TrackCount);
Assert.Equal("pl-deezer-12345", result[0].Id);
}
[Fact]
public async Task SearchPlaylistsAsync_WithLimit_RespectsLimit()
{
// Arrange
var deezerResponse = new
{
data = new[]
{
new
{
id = 12345,
title = "Playlist 1",
nb_tracks = 10,
picture_medium = "https://example.com/p1.jpg",
user = new { name = "User 1" }
}
}
};
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
// Act
var result = await _service.SearchPlaylistsAsync("test", 1);
// Assert
Assert.Single(result);
}
[Fact]
public async Task SearchPlaylistsAsync_WithEmptyResults_ReturnsEmptyList()
{
// Arrange
var deezerResponse = new
{
data = new object[] { }
};
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
// Act
var result = await _service.SearchPlaylistsAsync("nonexistent");
// Assert
Assert.Empty(result);
}
[Fact]
public async Task GetPlaylistAsync_WithValidId_ReturnsPlaylist()
{
// Arrange
var deezerResponse = new
{
id = 12345,
title = "Best Of Jazz",
description = "The best jazz tracks",
nb_tracks = 100,
picture_medium = "https://example.com/jazz.jpg",
user = new { name = "Jazz Lover" }
};
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
// Act
var result = await _service.GetPlaylistAsync("deezer", "12345");
// Assert
Assert.NotNull(result);
Assert.Equal("Best Of Jazz", result.Name);
Assert.Equal(100, result.TrackCount);
Assert.Equal("pl-deezer-12345", result.Id);
}
[Fact]
public async Task GetPlaylistAsync_WithWrongProvider_ReturnsNull()
{
// Act
var result = await _service.GetPlaylistAsync("qobuz", "12345");
// Assert
Assert.Null(result);
}
[Fact]
public async Task GetPlaylistTracksAsync_ReturnsListOfSongs()
{
// Arrange
var deezerResponse = new
{
tracks = new
{
data = new[]
{
new
{
id = 111,
title = "Track 1",
duration = 200,
track_position = 1,
disk_number = 1,
artist = new
{
id = 999,
name = "Artist A"
},
album = new
{
id = 888,
title = "Album X",
release_date = "2020-01-15",
cover_medium = "https://example.com/cover.jpg"
}
},
new
{
id = 222,
title = "Track 2",
duration = 180,
track_position = 2,
disk_number = 1,
artist = new
{
id = 777,
name = "Artist B"
},
album = new
{
id = 666,
title = "Album Y",
release_date = "2021-05-20",
cover_medium = "https://example.com/cover2.jpg"
}
}
}
}
};
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
// Act
var result = await _service.GetPlaylistTracksAsync("deezer", "12345");
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("Track 1", result[0].Title);
Assert.Equal("Artist A", result[0].Artist);
Assert.Equal("ext-deezer-111", result[0].Id);
}
[Fact]
public async Task GetPlaylistTracksAsync_WithWrongProvider_ReturnsEmptyList()
{
// Act
var result = await _service.GetPlaylistTracksAsync("qobuz", "12345");
// Assert
Assert.Empty(result);
}
[Fact]
public async Task GetPlaylistTracksAsync_WithEmptyPlaylist_ReturnsEmptyList()
{
// Arrange
var deezerResponse = new
{
tracks = new
{
data = new object[] { }
}
};
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
// Act
var result = await _service.GetPlaylistTracksAsync("deezer", "12345");
// Assert
Assert.Empty(result);
}
#endregion
}

View File

@@ -0,0 +1,375 @@
using octo_fiesta.Services.Common;
using Xunit;
namespace octo_fiesta.Tests;
public class PlaylistIdHelperTests
{
#region IsExternalPlaylist Tests
[Fact]
public void IsExternalPlaylist_WithValidPlaylistId_ReturnsTrue()
{
// Arrange
var id = "pl-deezer-123456";
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.True(result);
}
[Fact]
public void IsExternalPlaylist_WithValidQobuzPlaylistId_ReturnsTrue()
{
// Arrange
var id = "pl-qobuz-789012";
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.True(result);
}
[Fact]
public void IsExternalPlaylist_WithUpperCasePrefix_ReturnsTrue()
{
// Arrange
var id = "PL-deezer-123456";
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.True(result);
}
[Fact]
public void IsExternalPlaylist_WithRegularAlbumId_ReturnsFalse()
{
// Arrange
var id = "ext-deezer-album-123456";
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.False(result);
}
[Fact]
public void IsExternalPlaylist_WithNullId_ReturnsFalse()
{
// Arrange
string? id = null;
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.False(result);
}
[Fact]
public void IsExternalPlaylist_WithEmptyString_ReturnsFalse()
{
// Arrange
var id = "";
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.False(result);
}
[Fact]
public void IsExternalPlaylist_WithRandomString_ReturnsFalse()
{
// Arrange
var id = "random-string-123";
// Act
var result = PlaylistIdHelper.IsExternalPlaylist(id);
// Assert
Assert.False(result);
}
#endregion
#region ParsePlaylistId Tests
[Fact]
public void ParsePlaylistId_WithValidDeezerPlaylistId_ReturnsProviderAndExternalId()
{
// Arrange
var id = "pl-deezer-123456";
// Act
var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id);
// Assert
Assert.Equal("deezer", provider);
Assert.Equal("123456", externalId);
}
[Fact]
public void ParsePlaylistId_WithValidQobuzPlaylistId_ReturnsProviderAndExternalId()
{
// Arrange
var id = "pl-qobuz-789012";
// Act
var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id);
// Assert
Assert.Equal("qobuz", provider);
Assert.Equal("789012", externalId);
}
[Fact]
public void ParsePlaylistId_WithExternalIdContainingDashes_ParsesCorrectly()
{
// Arrange
var id = "pl-deezer-abc-def-123";
// Act
var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id);
// Assert
Assert.Equal("deezer", provider);
Assert.Equal("abc-def-123", externalId);
}
[Fact]
public void ParsePlaylistId_WithInvalidFormatNoProvider_ThrowsArgumentException()
{
// Arrange
var id = "pl-123456";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.ParsePlaylistId(id));
Assert.Contains("Invalid playlist ID format", exception.Message);
}
[Fact]
public void ParsePlaylistId_WithNonPlaylistId_ThrowsArgumentException()
{
// Arrange
var id = "ext-deezer-album-123456";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.ParsePlaylistId(id));
Assert.Contains("Invalid playlist ID format", exception.Message);
}
[Fact]
public void ParsePlaylistId_WithNullId_ThrowsArgumentException()
{
// Arrange
string? id = null;
// Act & Assert
Assert.Throws<ArgumentException>(() => PlaylistIdHelper.ParsePlaylistId(id!));
}
[Fact]
public void ParsePlaylistId_WithEmptyString_ThrowsArgumentException()
{
// Arrange
var id = "";
// Act & Assert
Assert.Throws<ArgumentException>(() => PlaylistIdHelper.ParsePlaylistId(id));
}
[Fact]
public void ParsePlaylistId_WithOnlyPrefix_ThrowsArgumentException()
{
// Arrange
var id = "pl-";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.ParsePlaylistId(id));
Assert.Contains("Invalid playlist ID format", exception.Message);
}
#endregion
#region CreatePlaylistId Tests
[Fact]
public void CreatePlaylistId_WithValidDeezerProviderAndId_ReturnsCorrectFormat()
{
// Arrange
var provider = "deezer";
var externalId = "123456";
// Act
var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId);
// Assert
Assert.Equal("pl-deezer-123456", result);
}
[Fact]
public void CreatePlaylistId_WithValidQobuzProviderAndId_ReturnsCorrectFormat()
{
// Arrange
var provider = "qobuz";
var externalId = "789012";
// Act
var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId);
// Assert
Assert.Equal("pl-qobuz-789012", result);
}
[Fact]
public void CreatePlaylistId_WithUpperCaseProvider_ConvertsToLowerCase()
{
// Arrange
var provider = "DEEZER";
var externalId = "123456";
// Act
var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId);
// Assert
Assert.Equal("pl-deezer-123456", result);
}
[Fact]
public void CreatePlaylistId_WithMixedCaseProvider_ConvertsToLowerCase()
{
// Arrange
var provider = "DeEzEr";
var externalId = "123456";
// Act
var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId);
// Assert
Assert.Equal("pl-deezer-123456", result);
}
[Fact]
public void CreatePlaylistId_WithExternalIdContainingDashes_PreservesDashes()
{
// Arrange
var provider = "deezer";
var externalId = "abc-def-123";
// Act
var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId);
// Assert
Assert.Equal("pl-deezer-abc-def-123", result);
}
[Fact]
public void CreatePlaylistId_WithNullProvider_ThrowsArgumentException()
{
// Arrange
string? provider = null;
var externalId = "123456";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.CreatePlaylistId(provider!, externalId));
Assert.Contains("Provider cannot be null or empty", exception.Message);
}
[Fact]
public void CreatePlaylistId_WithEmptyProvider_ThrowsArgumentException()
{
// Arrange
var provider = "";
var externalId = "123456";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.CreatePlaylistId(provider, externalId));
Assert.Contains("Provider cannot be null or empty", exception.Message);
}
[Fact]
public void CreatePlaylistId_WithNullExternalId_ThrowsArgumentException()
{
// Arrange
var provider = "deezer";
string? externalId = null;
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.CreatePlaylistId(provider, externalId!));
Assert.Contains("External ID cannot be null or empty", exception.Message);
}
[Fact]
public void CreatePlaylistId_WithEmptyExternalId_ThrowsArgumentException()
{
// Arrange
var provider = "deezer";
var externalId = "";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => PlaylistIdHelper.CreatePlaylistId(provider, externalId));
Assert.Contains("External ID cannot be null or empty", exception.Message);
}
#endregion
#region Round-Trip Tests
[Fact]
public void RoundTrip_CreateAndParse_ReturnsOriginalValues()
{
// Arrange
var originalProvider = "deezer";
var originalExternalId = "123456";
// Act
var playlistId = PlaylistIdHelper.CreatePlaylistId(originalProvider, originalExternalId);
var (parsedProvider, parsedExternalId) = PlaylistIdHelper.ParsePlaylistId(playlistId);
// Assert
Assert.Equal(originalProvider, parsedProvider);
Assert.Equal(originalExternalId, parsedExternalId);
}
[Fact]
public void RoundTrip_CreateWithUpperCaseAndParse_ReturnsLowerCaseProvider()
{
// Arrange
var originalProvider = "QOBUZ";
var originalExternalId = "789012";
// Act
var playlistId = PlaylistIdHelper.CreatePlaylistId(originalProvider, originalExternalId);
var (parsedProvider, parsedExternalId) = PlaylistIdHelper.ParsePlaylistId(playlistId);
// Assert
Assert.Equal("qobuz", parsedProvider); // Converted to lowercase
Assert.Equal(originalExternalId, parsedExternalId);
}
[Fact]
public void RoundTrip_WithComplexExternalId_PreservesValue()
{
// Arrange
var originalProvider = "deezer";
var originalExternalId = "abc-123-def-456";
// Act
var playlistId = PlaylistIdHelper.CreatePlaylistId(originalProvider, originalExternalId);
var (parsedProvider, parsedExternalId) = PlaylistIdHelper.ParsePlaylistId(playlistId);
// Assert
Assert.Equal(originalProvider, parsedProvider);
Assert.Equal(originalExternalId, parsedExternalId);
}
#endregion
}

View File

@@ -22,6 +22,7 @@ public class QobuzDownloadServiceTests : IDisposable
private readonly Mock<IMusicMetadataService> _metadataServiceMock;
private readonly Mock<ILogger<QobuzBundleService>> _bundleServiceLoggerMock;
private readonly Mock<ILogger<QobuzDownloadService>> _loggerMock;
private readonly Mock<IServiceProvider> _serviceProviderMock;
private readonly IConfiguration _configuration;
private readonly string _testDownloadPath;
private QobuzBundleService _bundleService;
@@ -41,6 +42,7 @@ public class QobuzDownloadServiceTests : IDisposable
_metadataServiceMock = new Mock<IMusicMetadataService>();
_bundleServiceLoggerMock = new Mock<ILogger<QobuzBundleService>>();
_loggerMock = new Mock<ILogger<QobuzDownloadService>>();
_serviceProviderMock = new Mock<IServiceProvider>();
// Create a real QobuzBundleService for testing (it will use the mocked HttpClient)
_bundleService = new QobuzBundleService(_httpClientFactoryMock.Object, _bundleServiceLoggerMock.Object);
@@ -94,6 +96,7 @@ public class QobuzDownloadServiceTests : IDisposable
_bundleService,
subsonicSettings,
qobuzSettings,
_serviceProviderMock.Object,
_loggerMock.Object);
}

View File

@@ -0,0 +1,659 @@
using octo_fiesta.Services.Qobuz;
using octo_fiesta.Models.Domain;
using octo_fiesta.Models.Settings;
using octo_fiesta.Models.Subsonic;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
using System.Net;
namespace octo_fiesta.Tests;
public class QobuzMetadataServiceTests
{
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
private readonly Mock<QobuzBundleService> _bundleServiceMock;
private readonly Mock<ILogger<QobuzMetadataService>> _loggerMock;
private readonly QobuzMetadataService _service;
public QobuzMetadataServiceTests()
{
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(_httpMessageHandlerMock.Object);
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
var httpClientFactory = Mock.Of<IHttpClientFactory>();
var bundleLogger = Mock.Of<ILogger<QobuzBundleService>>();
_bundleServiceMock = new Mock<QobuzBundleService>(httpClientFactory, bundleLogger);
_bundleServiceMock.Setup(b => b.GetAppIdAsync()).ReturnsAsync("fake-app-id-12345");
_loggerMock = new Mock<ILogger<QobuzMetadataService>>();
var subsonicSettings = Options.Create(new SubsonicSettings());
var qobuzSettings = Options.Create(new QobuzSettings
{
UserAuthToken = "fake-user-auth-token",
UserId = "8807208"
});
_service = new QobuzMetadataService(
_httpClientFactoryMock.Object,
subsonicSettings,
qobuzSettings,
_bundleServiceMock.Object,
_loggerMock.Object);
}
#region SearchPlaylistsAsync Tests
[Fact]
public async Task SearchPlaylistsAsync_WithValidQuery_ReturnsPlaylists()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""playlists"": {
""items"": [
{
""id"": 1578664,
""name"": ""Jazz Classics"",
""description"": ""Best of classic jazz music"",
""tracks_count"": 50,
""duration"": 12000,
""owner"": {
""name"": ""Qobuz Editorial""
},
""created_at"": 1609459200,
""images300"": [""https://example.com/cover.jpg""]
}
]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.SearchPlaylistsAsync("jazz", 20);
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("Jazz Classics", result[0].Name);
Assert.Equal("Best of classic jazz music", result[0].Description);
Assert.Equal(50, result[0].TrackCount);
Assert.Equal(12000, result[0].Duration);
Assert.Equal("qobuz", result[0].Provider);
Assert.Equal("1578664", result[0].ExternalId);
Assert.Equal("pl-qobuz-1578664", result[0].Id);
Assert.Equal("Qobuz Editorial", result[0].CuratorName);
}
[Fact]
public async Task SearchPlaylistsAsync_WithEmptyResults_ReturnsEmptyList()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""playlists"": {
""items"": []
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.SearchPlaylistsAsync("nonexistent", 20);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public async Task SearchPlaylistsAsync_WhenHttpFails_ReturnsEmptyList()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.SearchPlaylistsAsync("jazz", 20);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
#endregion
#region GetPlaylistAsync Tests
[Fact]
public async Task GetPlaylistAsync_WithValidId_ReturnsPlaylist()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""id"": 1578664,
""name"": ""Best Of Jazz"",
""description"": ""Top jazz tracks"",
""tracks_count"": 100,
""duration"": 24000,
""owner"": {
""name"": ""Qobuz Editor""
},
""created_at"": 1609459200,
""image_rectangle"": [""https://example.com/cover-large.jpg""]
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.GetPlaylistAsync("qobuz", "1578664");
// Assert
Assert.NotNull(result);
Assert.Equal("Best Of Jazz", result.Name);
Assert.Equal("Top jazz tracks", result.Description);
Assert.Equal(100, result.TrackCount);
Assert.Equal(24000, result.Duration);
Assert.Equal("pl-qobuz-1578664", result.Id);
Assert.Equal("Qobuz Editor", result.CuratorName);
Assert.Equal("https://example.com/cover-large.jpg", result.CoverUrl);
}
[Fact]
public async Task GetPlaylistAsync_WithWrongProvider_ReturnsNull()
{
// Act
var result = await _service.GetPlaylistAsync("deezer", "12345");
// Assert
Assert.Null(result);
}
#endregion
#region GetPlaylistTracksAsync Tests
[Fact]
public async Task GetPlaylistTracksAsync_WithValidId_ReturnsTracks()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""id"": 1578664,
""name"": ""My Jazz Playlist"",
""tracks"": {
""items"": [
{
""id"": 123456789,
""title"": ""Take Five"",
""duration"": 324,
""track_number"": 1,
""media_number"": 1,
""performer"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""album"": {
""id"": 222,
""title"": ""Time Out"",
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""image"": {
""thumbnail"": ""https://example.com/time-out.jpg""
}
}
},
{
""id"": 987654321,
""title"": ""So What"",
""duration"": 562,
""track_number"": 2,
""media_number"": 1,
""performer"": {
""id"": 333,
""name"": ""Miles Davis""
},
""album"": {
""id"": 444,
""title"": ""Kind of Blue"",
""artist"": {
""id"": 333,
""name"": ""Miles Davis""
},
""image"": {
""thumbnail"": ""https://example.com/kind-of-blue.jpg""
}
}
}
]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.GetPlaylistTracksAsync("qobuz", "1578664");
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
// First track
Assert.Equal("Take Five", result[0].Title);
Assert.Equal("Dave Brubeck Quartet", result[0].Artist);
Assert.Equal("My Jazz Playlist", result[0].Album); // Album should be playlist name
Assert.Equal(1, result[0].Track); // Track index starts at 1
Assert.Equal("ext-qobuz-song-123456789", result[0].Id);
Assert.Equal("qobuz", result[0].ExternalProvider);
Assert.Equal("123456789", result[0].ExternalId);
// Second track
Assert.Equal("So What", result[1].Title);
Assert.Equal("Miles Davis", result[1].Artist);
Assert.Equal("My Jazz Playlist", result[1].Album); // Album should be playlist name
Assert.Equal(2, result[1].Track); // Track index increments
Assert.Equal("ext-qobuz-song-987654321", result[1].Id);
}
[Fact]
public async Task GetPlaylistTracksAsync_WithWrongProvider_ReturnsEmptyList()
{
// Act
var result = await _service.GetPlaylistTracksAsync("deezer", "12345");
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public async Task GetPlaylistTracksAsync_WhenHttpFails_ReturnsEmptyList()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.GetPlaylistTracksAsync("qobuz", "999999");
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public async Task GetPlaylistTracksAsync_WithMissingPlaylistName_UsesDefaultName()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""id"": 1578664,
""tracks"": {
""items"": [
{
""id"": 123,
""title"": ""Test Track"",
""performer"": {
""id"": 1,
""name"": ""Test Artist""
},
""album"": {
""id"": 2,
""title"": ""Test Album"",
""artist"": {
""id"": 1,
""name"": ""Test Artist""
}
}
}
]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.GetPlaylistTracksAsync("qobuz", "1578664");
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("Unknown Playlist", result[0].Album);
}
#endregion
#region SearchSongsAsync Tests
[Fact]
public async Task SearchSongsAsync_WithValidQuery_ReturnsSongs()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""tracks"": {
""items"": [
{
""id"": 123456789,
""title"": ""Take Five"",
""duration"": 324,
""track_number"": 1,
""performer"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""album"": {
""id"": 222,
""title"": ""Time Out"",
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
}
}
}
]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.SearchSongsAsync("Take Five", 20);
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("Take Five", result[0].Title);
Assert.Equal("Dave Brubeck Quartet", result[0].Artist);
}
#endregion
#region SearchAlbumsAsync Tests
[Fact]
public async Task SearchAlbumsAsync_WithValidQuery_ReturnsAlbums()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""albums"": {
""items"": [
{
""id"": 222,
""title"": ""Time Out"",
""tracks_count"": 7,
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""release_date_original"": ""1959-12-14""
}
]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.SearchAlbumsAsync("Time Out", 20);
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("Time Out", result[0].Title);
Assert.Equal("Dave Brubeck Quartet", result[0].Artist);
Assert.Equal(1959, result[0].Year);
}
#endregion
#region GetSongAsync Tests
[Fact]
public async Task GetSongAsync_WithValidId_ReturnsSong()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""id"": 123456789,
""title"": ""Take Five"",
""duration"": 324,
""track_number"": 1,
""isrc"": ""USCO10300456"",
""copyright"": ""(P) 1959 Columbia Records"",
""performer"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""composer"": {
""id"": 999,
""name"": ""Paul Desmond""
},
""album"": {
""id"": 222,
""title"": ""Time Out"",
""tracks_count"": 7,
""release_date_original"": ""1959-12-14"",
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""genres_list"": [""Jazz"", ""Jazz→Cool Jazz""]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.GetSongAsync("qobuz", "123456789");
// Assert
Assert.NotNull(result);
Assert.Equal("Take Five", result.Title);
Assert.Equal("Dave Brubeck Quartet", result.Artist);
Assert.Equal("Time Out", result.Album);
Assert.Equal("USCO10300456", result.Isrc);
Assert.Equal("℗ 1959 Columbia Records", result.Copyright);
Assert.Equal(1959, result.Year);
Assert.Equal("1959-12-14", result.ReleaseDate);
Assert.Contains("Paul Desmond", result.Contributors);
Assert.Equal("Jazz, Cool Jazz", result.Genre);
}
[Fact]
public async Task GetSongAsync_WithWrongProvider_ReturnsNull()
{
// Act
var result = await _service.GetSongAsync("deezer", "123456789");
// Assert
Assert.Null(result);
}
#endregion
#region GetAlbumAsync Tests
[Fact]
public async Task GetAlbumAsync_WithValidId_ReturnsAlbumWithTracks()
{
// Arrange
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{
""id"": 222,
""title"": ""Time Out"",
""tracks_count"": 2,
""release_date_original"": ""1959-12-14"",
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""genres_list"": [""Jazz""],
""tracks"": {
""items"": [
{
""id"": 1,
""title"": ""Blue Rondo à la Turk"",
""track_number"": 1,
""performer"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""album"": {
""id"": 222,
""title"": ""Time Out"",
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
}
}
},
{
""id"": 2,
""title"": ""Take Five"",
""track_number"": 2,
""performer"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
},
""album"": {
""id"": 222,
""title"": ""Time Out"",
""artist"": {
""id"": 111,
""name"": ""Dave Brubeck Quartet""
}
}
}
]
}
}")
};
_httpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Act
var result = await _service.GetAlbumAsync("qobuz", "222");
// Assert
Assert.NotNull(result);
Assert.Equal("Time Out", result.Title);
Assert.Equal("Dave Brubeck Quartet", result.Artist);
Assert.Equal(1959, result.Year);
Assert.Equal(2, result.Songs.Count);
Assert.Equal("Blue Rondo à la Turk", result.Songs[0].Title);
Assert.Equal("Take Five", result.Songs[1].Title);
}
[Fact]
public async Task GetAlbumAsync_WithWrongProvider_ReturnsNull()
{
// Act
var result = await _service.GetAlbumAsync("deezer", "222");
// Assert
Assert.Null(result);
}
#endregion
}

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
using Moq;
using octo_fiesta.Models.Domain;
using octo_fiesta.Models.Search;
using octo_fiesta.Models.Subsonic;
using octo_fiesta.Services.Subsonic;
using System.Text;
using System.Text.Json;
@@ -187,39 +188,12 @@ public class SubsonicModelMapperTests
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
localSongs, new List<object>(), new List<object>(), externalResult, true);
localSongs, new List<object>(), new List<object>(), externalResult, new List<ExternalPlaylist>(), true);
// Assert
Assert.Equal(2, mergedSongs.Count);
}
[Fact]
public void MergeSearchResults_Json_DeduplicatesArtists()
{
// Arrange
var localArtists = new List<object>
{
new Dictionary<string, object> { ["id"] = "local1", ["name"] = "Test Artist" }
};
var externalResult = new SearchResult
{
Songs = new List<Song>(),
Albums = new List<Album>(),
Artists = new List<Artist>
{
new Artist { Id = "ext1", Name = "Test Artist" }, // Same name - should be filtered
new Artist { Id = "ext2", Name = "Different Artist" } // Different name - should be included
}
};
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
new List<object>(), new List<object>(), localArtists, externalResult, true);
// Assert
Assert.Equal(2, mergedArtists.Count); // 1 local + 1 external (duplicate filtered)
}
[Fact]
public void MergeSearchResults_Json_CaseInsensitiveDeduplication()
{
@@ -240,7 +214,7 @@ public class SubsonicModelMapperTests
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
new List<object>(), new List<object>(), localArtists, externalResult, true);
new List<object>(), new List<object>(), localArtists, externalResult, new List<ExternalPlaylist>(), true);
// Assert
Assert.Single(mergedArtists); // Only the local artist
@@ -267,7 +241,7 @@ public class SubsonicModelMapperTests
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
localSongs, new List<object>(), new List<object>(), externalResult, false);
localSongs, new List<object>(), new List<object>(), externalResult, new List<ExternalPlaylist>(), false);
// Assert
Assert.Equal(2, mergedSongs.Count);
@@ -294,7 +268,7 @@ public class SubsonicModelMapperTests
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
new List<object>(), new List<object>(), localArtists, externalResult, false);
new List<object>(), new List<object>(), localArtists, externalResult, new List<ExternalPlaylist>(), false);
// Assert
Assert.Equal(2, mergedArtists.Count); // 1 local + 1 external (duplicate filtered)
@@ -313,7 +287,7 @@ public class SubsonicModelMapperTests
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
new List<object>(), new List<object>(), new List<object>(), externalResult, true);
new List<object>(), new List<object>(), new List<object>(), externalResult, new List<ExternalPlaylist>(), true);
// Assert
Assert.Single(mergedSongs);
@@ -337,7 +311,7 @@ public class SubsonicModelMapperTests
// Act
var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults(
localSongs, localAlbums, localArtists, externalResult, true);
localSongs, localAlbums, localArtists, externalResult, new List<ExternalPlaylist>(), true);
// Assert
Assert.Single(mergedSongs);