mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-07-02 18:46:44 -04:00
1159 lines
38 KiB
C#
1159 lines
38 KiB
C#
using allstarr.Services.Deezer;
|
|
using allstarr.Models.Domain;
|
|
using allstarr.Models.Settings;
|
|
using allstarr.Models.Download;
|
|
using allstarr.Models.Search;
|
|
using allstarr.Models.Subsonic;
|
|
using Moq;
|
|
using Moq.Protected;
|
|
using Microsoft.Extensions.Options;
|
|
using System.Net;
|
|
using System.Text.Json;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public class DeezerMetadataServiceTests
|
|
{
|
|
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
|
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
|
private readonly SubsonicSettings _settings;
|
|
private DeezerMetadataService _service;
|
|
|
|
public DeezerMetadataServiceTests()
|
|
{
|
|
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
|
var httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
|
|
|
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
|
|
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
|
|
|
_settings = new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly };
|
|
_service = CreateService(_settings);
|
|
}
|
|
|
|
private DeezerMetadataService CreateService(SubsonicSettings settings)
|
|
{
|
|
var options = Options.Create(settings);
|
|
var deezerOptions = Options.Create(new DeezerSettings { MinRequestIntervalMs = 0 });
|
|
return new DeezerMetadataService(_httpClientFactoryMock.Object, options, deezerSettings: deezerOptions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_ReturnsListOfSongs()
|
|
{
|
|
// Arrange
|
|
var deezerResponse = new
|
|
{
|
|
data = new[]
|
|
{
|
|
new
|
|
{
|
|
id = 123456,
|
|
title = "Test Song",
|
|
isrc = "TESTISRC1234",
|
|
duration = 180,
|
|
track_position = 1,
|
|
artist = new { id = 789, name = "Test Artist" },
|
|
album = new { id = 456, title = "Test Album", cover_medium = "https://example.com/cover.jpg" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchSongsAsync("test query", 20);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("ext-deezer-song-123456", result[0].Id);
|
|
Assert.Equal("Test Song", result[0].Title);
|
|
Assert.Equal("Test Artist", result[0].Artist);
|
|
Assert.Equal("Test Album", result[0].Album);
|
|
Assert.Equal(180, result[0].Duration);
|
|
Assert.Equal("TESTISRC1234", result[0].Isrc);
|
|
Assert.False(result[0].IsLocal);
|
|
Assert.Equal("deezer", result[0].ExternalProvider);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_AmpersandVariant_PreservesProviderOrderAndDeduplicates()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
var pathAndQuery = request.RequestUri!.PathAndQuery;
|
|
requests.Add(pathAndQuery);
|
|
|
|
var response = pathAndQuery.Contains("q=love%20and%20hyperbole", StringComparison.Ordinal)
|
|
? new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(2, "Shared Result"),
|
|
CreateTrackSearchResult(3, "Variant Result")
|
|
}
|
|
}
|
|
: new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(1, "Original Result"),
|
|
CreateTrackSearchResult(2, "Shared Result")
|
|
}
|
|
};
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(response));
|
|
});
|
|
|
|
var result = await _service.SearchSongsAsync("love & hyperbole", 3);
|
|
|
|
Assert.Equal(["Original Result", "Shared Result", "Variant Result"], result.Select(song => song.Title));
|
|
Assert.Equal(2, requests.Count);
|
|
Assert.Contains("q=love%20%26%20hyperbole&limit=3&order=RANKING", requests[0]);
|
|
Assert.Contains("q=love%20and%20hyperbole&limit=3&order=RANKING", requests[1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchAlbumsAsync_ReturnsListOfAlbums()
|
|
{
|
|
// Arrange
|
|
var deezerResponse = new
|
|
{
|
|
data = new[]
|
|
{
|
|
new
|
|
{
|
|
id = 456789,
|
|
title = "Test Album",
|
|
nb_tracks = 12,
|
|
release_date = "2023-01-15",
|
|
cover_medium = "https://example.com/album.jpg",
|
|
artist = new { id = 123, name = "Test Artist" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchAlbumsAsync("test album", 20);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("ext-deezer-album-456789", result[0].Id);
|
|
Assert.Equal("Test Album", result[0].Title);
|
|
Assert.Equal("Test Artist", result[0].Artist);
|
|
Assert.Equal(12, result[0].SongCount);
|
|
Assert.Equal(2023, result[0].Year);
|
|
Assert.False(result[0].IsLocal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchArtistsAsync_ReturnsListOfArtists()
|
|
{
|
|
// Arrange
|
|
var deezerResponse = new
|
|
{
|
|
data = new[]
|
|
{
|
|
new
|
|
{
|
|
id = 789012,
|
|
name = "Test Artist",
|
|
nb_album = 5,
|
|
picture_medium = "https://example.com/artist.jpg"
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchArtistsAsync("test artist", 20);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("ext-deezer-artist-789012", result[0].Id);
|
|
Assert.Equal("Test Artist", result[0].Name);
|
|
Assert.Equal(5, result[0].AlbumCount);
|
|
Assert.False(result[0].IsLocal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchAllAsync_ReturnsAllTypes()
|
|
{
|
|
// This test would need multiple HTTP calls mocked, simplified for now
|
|
var emptyResponse = JsonSerializer.Serialize(new { data = Array.Empty<object>() });
|
|
SetupHttpResponse(emptyResponse);
|
|
|
|
// Act
|
|
var result = await _service.SearchAllAsync("test");
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Songs);
|
|
Assert.NotNull(result.Albums);
|
|
Assert.NotNull(result.Artists);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchAllAsync_AmpersandQuery_UsesVariantsForEachRequestedBucket()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
lock (requests)
|
|
{
|
|
requests.Add(request.RequestUri!.PathAndQuery);
|
|
}
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new { data = Array.Empty<object>() }));
|
|
});
|
|
|
|
await _service.SearchAllAsync("love & hyperbole", songLimit: 1, albumLimit: 1, artistLimit: 1);
|
|
|
|
Assert.Contains(requests, request =>
|
|
request.Contains("/search/track?q=love%20%26%20hyperbole&limit=1", StringComparison.Ordinal));
|
|
Assert.Contains(requests, request =>
|
|
request.Contains("/search/track?q=love%20and%20hyperbole&limit=1", StringComparison.Ordinal));
|
|
Assert.Contains(requests, request =>
|
|
request.Contains("/search/album?q=love%20%26%20hyperbole&limit=1", StringComparison.Ordinal));
|
|
Assert.Contains(requests, request =>
|
|
request.Contains("/search/album?q=love%20and%20hyperbole&limit=1", StringComparison.Ordinal));
|
|
Assert.Contains(requests, request =>
|
|
request.Contains("/search/artist?q=love%20%26%20hyperbole&limit=1", StringComparison.Ordinal));
|
|
Assert.Contains(requests, request =>
|
|
request.Contains("/search/artist?q=love%20and%20hyperbole&limit=1", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FindSongByIsrcAsync_UsesExactTrackEndpoint()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
requests.Add(request.RequestUri!.PathAndQuery);
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
id = 116348632,
|
|
title = "Hey Jude",
|
|
isrc = "GBUM71505902",
|
|
duration = 429,
|
|
track_position = 21,
|
|
disk_number = 1,
|
|
artist = new { id = 1, name = "The Beatles" },
|
|
album = new
|
|
{
|
|
id = 12047956,
|
|
title = "1",
|
|
cover_medium = "https://example.com/cover.jpg"
|
|
}
|
|
}));
|
|
});
|
|
|
|
var result = await _service.FindSongByIsrcAsync(" GBUM71505902 ");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("ext-deezer-song-116348632", result.Id);
|
|
Assert.Equal("GBUM71505902", result.Isrc);
|
|
Assert.Equal(["/track/isrc:GBUM71505902"], requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSongAsync_WithDeezerProvider_ReturnsSong()
|
|
{
|
|
// Arrange
|
|
var deezerResponse = new
|
|
{
|
|
id = 123456,
|
|
title = "Test Song",
|
|
duration = 200,
|
|
track_position = 3,
|
|
artist = new { id = 789, name = "Test Artist" },
|
|
album = new { id = 456, title = "Test Album", cover_medium = "https://example.com/cover.jpg" }
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.GetSongAsync("deezer", "123456");
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("ext-deezer-song-123456", result.Id);
|
|
Assert.Equal("Test Song", result.Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSongAsync_WithNonDeezerProvider_ReturnsNull()
|
|
{
|
|
// Act
|
|
var result = await _service.GetSongAsync("spotify", "123456");
|
|
|
|
// Assert
|
|
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-album-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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAlbumAsync_PaginatesTracklistWhenAlbumDetailIsPartial()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
var pathAndQuery = request.RequestUri!.PathAndQuery;
|
|
requests.Add(pathAndQuery);
|
|
|
|
if (pathAndQuery.Contains("index=1", StringComparison.Ordinal))
|
|
{
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(222, "Track 2")
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (pathAndQuery.Contains("/tracks", StringComparison.Ordinal))
|
|
{
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(111, "Track 1")
|
|
},
|
|
next = "https://api.deezer.com/album/456789/tracks?limit=100&index=1"
|
|
}));
|
|
}
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
id = 456789,
|
|
title = "Paged Album",
|
|
nb_tracks = 2,
|
|
artist = new { id = 123, name = "Test Artist" },
|
|
tracks = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(111, "Track 1")
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
|
|
var result = await _service.GetAlbumAsync("deezer", "456789");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(["Track 1", "Track 2"], result.Songs.Select(song => song.Title));
|
|
Assert.Contains("/album/456789/tracks?index=0&limit=100", requests);
|
|
Assert.Contains("/album/456789/tracks?limit=100&index=1", requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetArtistAlbumsAsync_FallsBackToDocumentedIndexPagination()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
var pathAndQuery = request.RequestUri!.PathAndQuery;
|
|
requests.Add(pathAndQuery);
|
|
|
|
if (pathAndQuery.Contains("index=1", StringComparison.Ordinal))
|
|
{
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 2002,
|
|
title = "Second Album",
|
|
nb_tracks = 8,
|
|
artist = new { id = 27, name = "Artist" }
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 2001,
|
|
title = "First Album",
|
|
nb_tracks = 10,
|
|
artist = new { id = 27, name = "Artist" }
|
|
}
|
|
},
|
|
total = 2
|
|
}));
|
|
});
|
|
|
|
var result = await _service.GetArtistAlbumsAsync("deezer", "27");
|
|
|
|
Assert.Equal(["First Album", "Second Album"], result.Select(album => album.Title));
|
|
Assert.Contains("/artist/27/albums?index=0&limit=100", requests);
|
|
Assert.Contains("/artist/27/albums?index=1&limit=100", requests);
|
|
}
|
|
|
|
private void SetupHttpResponse(string content, HttpStatusCode statusCode = HttpStatusCode.OK)
|
|
{
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ReturnsAsync(new HttpResponseMessage
|
|
{
|
|
StatusCode = statusCode,
|
|
Content = new StringContent(content)
|
|
});
|
|
}
|
|
|
|
private void SetupHttpResponse(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
|
|
{
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Returns((HttpRequestMessage request, CancellationToken _) =>
|
|
Task.FromResult(responseFactory(request)));
|
|
}
|
|
|
|
private static HttpResponseMessage CreateJsonResponse(string content)
|
|
{
|
|
return new HttpResponseMessage
|
|
{
|
|
StatusCode = HttpStatusCode.OK,
|
|
Content = new StringContent(content)
|
|
};
|
|
}
|
|
|
|
private static object CreateTrackSearchResult(long id, string title)
|
|
{
|
|
return new
|
|
{
|
|
id,
|
|
title,
|
|
duration = 180,
|
|
artist = new { id = 789, name = "Test Artist" },
|
|
album = new { id = 456, title = "Test Album", cover_medium = "https://example.com/cover.jpg" }
|
|
};
|
|
}
|
|
|
|
private static object CreatePlaylistSearchResult(long id, string title)
|
|
{
|
|
return new
|
|
{
|
|
id,
|
|
title,
|
|
nb_tracks = 10,
|
|
picture_medium = "https://example.com/playlist.jpg",
|
|
user = new { name = "Playlist User" }
|
|
};
|
|
}
|
|
|
|
#region Explicit Filter Tests
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_ExplicitOnlyFilter_ExcludesCleanVersions()
|
|
{
|
|
// Arrange
|
|
_service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly });
|
|
|
|
var deezerResponse = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 1,
|
|
title = "Explicit Original",
|
|
duration = 180,
|
|
explicit_content_lyrics = 1, // Explicit
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 2,
|
|
title = "Clean Version",
|
|
duration = 180,
|
|
explicit_content_lyrics = 3, // Clean/edited - should be excluded
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 3,
|
|
title = "Naturally Clean",
|
|
duration = 180,
|
|
explicit_content_lyrics = 0, // Naturally clean - should be included
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchSongsAsync("test", 20);
|
|
|
|
// Assert
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(result, s => s.Title == "Explicit Original");
|
|
Assert.Contains(result, s => s.Title == "Naturally Clean");
|
|
Assert.DoesNotContain(result, s => s.Title == "Clean Version");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_CleanOnlyFilter_ExcludesExplicitContent()
|
|
{
|
|
// Arrange
|
|
_service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.CleanOnly });
|
|
|
|
var deezerResponse = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 1,
|
|
title = "Explicit Original",
|
|
duration = 180,
|
|
explicit_content_lyrics = 1, // Explicit - should be excluded
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 2,
|
|
title = "Clean Version",
|
|
duration = 180,
|
|
explicit_content_lyrics = 3, // Clean/edited - should be included
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 3,
|
|
title = "Naturally Clean",
|
|
duration = 180,
|
|
explicit_content_lyrics = 0, // Naturally clean - should be included
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchSongsAsync("test", 20);
|
|
|
|
// Assert
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(result, s => s.Title == "Clean Version");
|
|
Assert.Contains(result, s => s.Title == "Naturally Clean");
|
|
Assert.DoesNotContain(result, s => s.Title == "Explicit Original");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_AllFilter_IncludesEverything()
|
|
{
|
|
// Arrange
|
|
_service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.All });
|
|
|
|
var deezerResponse = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 1,
|
|
title = "Explicit Original",
|
|
duration = 180,
|
|
explicit_content_lyrics = 1,
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 2,
|
|
title = "Clean Version",
|
|
duration = 180,
|
|
explicit_content_lyrics = 3,
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 3,
|
|
title = "Naturally Clean",
|
|
duration = 180,
|
|
explicit_content_lyrics = 0,
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchSongsAsync("test", 20);
|
|
|
|
// Assert
|
|
Assert.Equal(3, result.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_ExplicitOnlyFilter_IncludesTracksWithNoExplicitInfo()
|
|
{
|
|
// Arrange
|
|
_service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly });
|
|
|
|
var deezerResponse = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 1,
|
|
title = "No Explicit Info",
|
|
duration = 180,
|
|
// No explicit_content_lyrics field
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchSongsAsync("test", 20);
|
|
|
|
// Assert
|
|
Assert.Single(result);
|
|
Assert.Equal("No Explicit Info", result[0].Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAlbumAsync_ExplicitOnlyFilter_FiltersAlbumTracks()
|
|
{
|
|
// Arrange
|
|
_service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly });
|
|
|
|
var deezerResponse = new
|
|
{
|
|
id = 456789,
|
|
title = "Test Album",
|
|
nb_tracks = 3,
|
|
release_date = "2023-05-20",
|
|
cover_medium = "https://example.com/album.jpg",
|
|
artist = new { id = 123, name = "Test Artist" },
|
|
tracks = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 111,
|
|
title = "Explicit Track",
|
|
duration = 180,
|
|
explicit_content_lyrics = 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 = "Clean Version Track",
|
|
duration = 200,
|
|
explicit_content_lyrics = 3, // Should be excluded
|
|
artist = new { id = 123, name = "Test Artist" },
|
|
album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" }
|
|
},
|
|
new
|
|
{
|
|
id = 333,
|
|
title = "Naturally Clean Track",
|
|
duration = 220,
|
|
explicit_content_lyrics = 0,
|
|
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(2, result.Songs.Count);
|
|
Assert.Contains(result.Songs, s => s.Title == "Explicit Track");
|
|
Assert.Contains(result.Songs, s => s.Title == "Naturally Clean Track");
|
|
Assert.DoesNotContain(result.Songs, s => s.Title == "Clean Version Track");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchSongsAsync_ParsesExplicitContentLyrics()
|
|
{
|
|
// Arrange
|
|
var deezerResponse = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
new
|
|
{
|
|
id = 1,
|
|
title = "Test Track",
|
|
duration = 180,
|
|
explicit_content_lyrics = 1,
|
|
artist = new { id = 100, name = "Artist" },
|
|
album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" }
|
|
}
|
|
}
|
|
};
|
|
|
|
SetupHttpResponse(JsonSerializer.Serialize(deezerResponse));
|
|
|
|
// Act
|
|
var result = await _service.SearchSongsAsync("test", 20);
|
|
|
|
// Assert
|
|
Assert.Single(result);
|
|
Assert.Equal(1, result[0].ExplicitContentLyrics);
|
|
}
|
|
|
|
#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("ext-deezer-playlist-12345", result[0].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchPlaylistsAsync_AmpersandVariant_PreservesProviderOrderAndDeduplicates()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
var pathAndQuery = request.RequestUri!.PathAndQuery;
|
|
requests.Add(pathAndQuery);
|
|
|
|
var response = pathAndQuery.Contains("q=love%20and%20hyperbole", StringComparison.Ordinal)
|
|
? new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreatePlaylistSearchResult(2, "Shared Playlist"),
|
|
CreatePlaylistSearchResult(3, "Variant Playlist")
|
|
}
|
|
}
|
|
: new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreatePlaylistSearchResult(1, "Original Playlist"),
|
|
CreatePlaylistSearchResult(2, "Shared Playlist")
|
|
}
|
|
};
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(response));
|
|
});
|
|
|
|
var result = await _service.SearchPlaylistsAsync("love & hyperbole", 3);
|
|
|
|
Assert.Equal(["Original Playlist", "Shared Playlist", "Variant Playlist"], result.Select(playlist => playlist.Name));
|
|
Assert.Equal(2, requests.Count);
|
|
Assert.Contains("q=love%20%26%20hyperbole&limit=3&order=RANKING", requests[0]);
|
|
Assert.Contains("q=love%20and%20hyperbole&limit=3&order=RANKING", requests[1]);
|
|
}
|
|
|
|
[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("ext-deezer-playlist-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",
|
|
isrc = "TESTISRC0001",
|
|
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",
|
|
isrc = "TESTISRC0002",
|
|
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-song-111", result[0].Id);
|
|
Assert.Equal("TESTISRC0001", result[0].Isrc);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetPlaylistTracksAsync_WithWrongProvider_ReturnsEmptyList()
|
|
{
|
|
// Act
|
|
var result = await _service.GetPlaylistTracksAsync("qobuz", "12345");
|
|
|
|
// Assert
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetPlaylistTracksAsync_PaginatesTracklistWhenPlaylistDetailIsPartial()
|
|
{
|
|
var requests = new List<string>();
|
|
SetupHttpResponse(request =>
|
|
{
|
|
var pathAndQuery = request.RequestUri!.PathAndQuery;
|
|
requests.Add(pathAndQuery);
|
|
|
|
if (pathAndQuery.Contains("index=1", StringComparison.Ordinal))
|
|
{
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(222, "Track 2")
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (pathAndQuery.Contains("/tracks", StringComparison.Ordinal))
|
|
{
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(111, "Track 1")
|
|
},
|
|
next = "https://api.deezer.com/playlist/12345/tracks?limit=100&index=1"
|
|
}));
|
|
}
|
|
|
|
return CreateJsonResponse(JsonSerializer.Serialize(new
|
|
{
|
|
id = 12345,
|
|
title = "Paged Playlist",
|
|
nb_tracks = 2,
|
|
tracks = new
|
|
{
|
|
data = new object[]
|
|
{
|
|
CreateTrackSearchResult(111, "Track 1")
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
|
|
var result = await _service.GetPlaylistTracksAsync("deezer", "12345");
|
|
|
|
Assert.Equal(["Track 1", "Track 2"], result.Select(song => song.Title));
|
|
Assert.All(result, song => Assert.Equal("Paged Playlist", song.Album));
|
|
Assert.Equal(1, result[0].Track);
|
|
Assert.Equal(2, result[1].Track);
|
|
Assert.Contains("/playlist/12345/tracks?index=0&limit=100", requests);
|
|
Assert.Contains("/playlist/12345/tracks?limit=100&index=1", requests);
|
|
}
|
|
|
|
[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
|
|
}
|