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 _httpClientFactoryMock; private readonly Mock _httpMessageHandlerMock; private readonly Mock _localLibraryServiceMock; private readonly Mock _metadataServiceMock; private readonly Mock> _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(); var httpClient = new HttpClient(_httpMessageHandlerMock.Object); _httpClientFactoryMock = new Mock(); _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); _localLibraryServiceMock = new Mock(); _metadataServiceMock = new Mock(); _loggerMock = new Mock>(); _configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["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 { ["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(() => 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(() => service.DownloadSongAsync("deezer", "999999")); Assert.Equal("Song not found", exception.Message); } } /// /// Unit tests for the PathHelper class that handles file organization logic. /// public class PathHelperTests : IDisposable { private readonly string _testPath; public PathHelperTests() { _testPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-pathhelper-tests-" + Guid.NewGuid()); Directory.CreateDirectory(_testPath); } public void Dispose() { if (Directory.Exists(_testPath)) { Directory.Delete(_testPath, true); } } #region SanitizeFileName Tests [Fact] public void SanitizeFileName_WithValidName_ReturnsUnchanged() { // Arrange & Act var result = PathHelper.SanitizeFileName("My Song Title"); // Assert Assert.Equal("My Song Title", result); } [Fact] public void SanitizeFileName_WithInvalidChars_ReplacesWithUnderscore() { // Arrange - Use forward slash which is invalid on all platforms var result = PathHelper.SanitizeFileName("Song/With/Invalid"); // Assert - Check that forward slashes were replaced with underscores Assert.Equal("Song_With_Invalid", result); } [Fact] public void SanitizeFileName_WithNullOrEmpty_ReturnsUnknown() { // Arrange & Act var resultNull = PathHelper.SanitizeFileName(null!); var resultEmpty = PathHelper.SanitizeFileName(""); var resultWhitespace = PathHelper.SanitizeFileName(" "); // Assert Assert.Equal("Unknown", resultNull); Assert.Equal("Unknown", resultEmpty); Assert.Equal("Unknown", resultWhitespace); } [Fact] public void SanitizeFileName_WithLongName_TruncatesTo100Chars() { // Arrange var longName = new string('A', 150); // Act var result = PathHelper.SanitizeFileName(longName); // Assert Assert.Equal(100, result.Length); } #endregion #region SanitizeFolderName Tests [Fact] public void SanitizeFolderName_WithValidName_ReturnsUnchanged() { // Arrange & Act var result = PathHelper.SanitizeFolderName("Artist Name"); // Assert Assert.Equal("Artist Name", result); } [Fact] public void SanitizeFolderName_WithNullOrEmpty_ReturnsUnknown() { // Arrange & Act var resultNull = PathHelper.SanitizeFolderName(null!); var resultEmpty = PathHelper.SanitizeFolderName(""); var resultWhitespace = PathHelper.SanitizeFolderName(" "); // Assert Assert.Equal("Unknown", resultNull); Assert.Equal("Unknown", resultEmpty); Assert.Equal("Unknown", resultWhitespace); } [Fact] public void SanitizeFolderName_WithTrailingDots_RemovesDots() { // Arrange & Act var result = PathHelper.SanitizeFolderName("Artist Name..."); // Assert Assert.Equal("Artist Name", result); } [Fact] public void SanitizeFolderName_WithInvalidChars_ReplacesWithUnderscore() { // Arrange - Use forward slash which is invalid on all platforms var result = PathHelper.SanitizeFolderName("Artist/With/Invalid"); // Assert - Check that forward slashes were replaced with underscores Assert.Equal("Artist_With_Invalid", result); } #endregion #region BuildTrackPath Tests [Fact] public void BuildTrackPath_WithAllParameters_CreatesCorrectStructure() { // Arrange var downloadPath = "/downloads"; var artist = "Test Artist"; var album = "Test Album"; var title = "Test Song"; var trackNumber = 5; var extension = ".mp3"; // Act var result = PathHelper.BuildTrackPath(downloadPath, artist, album, title, trackNumber, extension); // Assert Assert.Contains("Test Artist", result); Assert.Contains("Test Album", result); Assert.Contains("05 - Test Song.mp3", result); } [Fact] public void BuildTrackPath_WithoutTrackNumber_OmitsTrackPrefix() { // Arrange var downloadPath = "/downloads"; var artist = "Test Artist"; var album = "Test Album"; var title = "Test Song"; var extension = ".mp3"; // Act var result = PathHelper.BuildTrackPath(downloadPath, artist, album, title, null, extension); // Assert Assert.Contains("Test Song.mp3", result); Assert.DoesNotContain(" - Test Song", result.Split(Path.DirectorySeparatorChar).Last()); } [Fact] public void BuildTrackPath_WithSingleDigitTrack_PadsWithZero() { // Arrange & Act var result = PathHelper.BuildTrackPath("/downloads", "Artist", "Album", "Song", 3, ".mp3"); // Assert Assert.Contains("03 - Song.mp3", result); } [Fact] public void BuildTrackPath_WithFlacExtension_UsesFlacExtension() { // Arrange & Act var result = PathHelper.BuildTrackPath("/downloads", "Artist", "Album", "Song", 1, ".flac"); // Assert Assert.EndsWith(".flac", result); } [Fact] public void BuildTrackPath_CreatesArtistAlbumHierarchy() { // Arrange & Act var result = PathHelper.BuildTrackPath("/downloads", "My Artist", "My Album", "My Song", 1, ".mp3"); // Assert // Verify the structure is: downloadPath/Artist/Album/track.mp3 var parts = result.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); Assert.Contains("My Artist", parts); Assert.Contains("My Album", parts); // Artist should come before Album in the path var artistIndex = Array.IndexOf(parts, "My Artist"); var albumIndex = Array.IndexOf(parts, "My Album"); Assert.True(artistIndex < albumIndex, "Artist folder should be parent of Album folder"); } #endregion #region ResolveUniquePath Tests [Fact] public void ResolveUniquePath_WhenFileDoesNotExist_ReturnsSamePath() { // Arrange var path = Path.Combine(_testPath, "nonexistent.mp3"); // Act var result = PathHelper.ResolveUniquePath(path); // Assert Assert.Equal(path, result); } [Fact] public void ResolveUniquePath_WhenFileExists_ReturnsPathWithCounter() { // Arrange var basePath = Path.Combine(_testPath, "existing.mp3"); File.WriteAllText(basePath, "content"); // Act var result = PathHelper.ResolveUniquePath(basePath); // Assert Assert.NotEqual(basePath, result); Assert.Contains("existing (1).mp3", result); } [Fact] public void ResolveUniquePath_WhenMultipleFilesExist_IncrementsCounter() { // Arrange var basePath = Path.Combine(_testPath, "song.mp3"); var path1 = Path.Combine(_testPath, "song (1).mp3"); File.WriteAllText(basePath, "content"); File.WriteAllText(path1, "content"); // Act var result = PathHelper.ResolveUniquePath(basePath); // Assert Assert.Contains("song (2).mp3", result); } #endregion }