From ad15e10ea6477e45156fcb97dcfd144ecbe321fc Mon Sep 17 00:00:00 2001 From: V1ck3s Date: Mon, 8 Dec 2025 15:15:37 +0100 Subject: [PATCH] test: add unit tests for DeezerMetadataService and LocalLibraryService - Add DeezerMetadataServiceTests with mocked HTTP responses - Add LocalLibraryServiceTests for song ID parsing and registration - Configure xUnit test project with Moq and MVC Testing packages --- .../DeezerMetadataServiceTests.cs | 198 ++++++++++++++++++ octo-fiesta.Tests/LocalLibraryServiceTests.cs | 155 ++++++++++++++ octo-fiesta.Tests/octo-fiesta.Tests.csproj | 28 +++ octo-fiesta.sln | 61 ++++-- 4 files changed, 426 insertions(+), 16 deletions(-) create mode 100644 octo-fiesta.Tests/DeezerMetadataServiceTests.cs create mode 100644 octo-fiesta.Tests/LocalLibraryServiceTests.cs create mode 100644 octo-fiesta.Tests/octo-fiesta.Tests.csproj diff --git a/octo-fiesta.Tests/DeezerMetadataServiceTests.cs b/octo-fiesta.Tests/DeezerMetadataServiceTests.cs new file mode 100644 index 0000000..90a04db --- /dev/null +++ b/octo-fiesta.Tests/DeezerMetadataServiceTests.cs @@ -0,0 +1,198 @@ +using octo_fiesta.Services; +using octo_fiesta.Models; +using Moq; +using Moq.Protected; +using System.Net; +using System.Text.Json; + +namespace octo_fiesta.Tests; + +public class DeezerMetadataServiceTests +{ + private readonly Mock _httpClientFactoryMock; + private readonly Mock _httpMessageHandlerMock; + private readonly DeezerMetadataService _service; + + public DeezerMetadataServiceTests() + { + _httpMessageHandlerMock = new Mock(); + var httpClient = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock = new Mock(); + _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); + + _service = new DeezerMetadataService(_httpClientFactoryMock.Object); + } + + [Fact] + public async Task SearchSongsAsync_ReturnsListOfSongs() + { + // Arrange + var deezerResponse = new + { + data = new[] + { + new + { + id = 123456, + title = "Test Song", + 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-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.False(result[0].IsLocal); + Assert.Equal("deezer", result[0].ExternalProvider); + } + + [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-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-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() }); + 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 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-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); + } + + private void SetupHttpResponse(string content, HttpStatusCode statusCode = HttpStatusCode.OK) + { + _httpMessageHandlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = statusCode, + Content = new StringContent(content) + }); + } +} diff --git a/octo-fiesta.Tests/LocalLibraryServiceTests.cs b/octo-fiesta.Tests/LocalLibraryServiceTests.cs new file mode 100644 index 0000000..a9f5d22 --- /dev/null +++ b/octo-fiesta.Tests/LocalLibraryServiceTests.cs @@ -0,0 +1,155 @@ +using octo_fiesta.Services; +using octo_fiesta.Models; +using Microsoft.Extensions.Configuration; + +namespace octo_fiesta.Tests; + +public class LocalLibraryServiceTests : IDisposable +{ + private readonly LocalLibraryService _service; + private readonly string _testDownloadPath; + + public LocalLibraryServiceTests() + { + _testDownloadPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-tests-" + Guid.NewGuid()); + Directory.CreateDirectory(_testDownloadPath); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Library:DownloadPath"] = _testDownloadPath + }) + .Build(); + + _service = new LocalLibraryService(configuration); + } + + public void Dispose() + { + if (Directory.Exists(_testDownloadPath)) + { + Directory.Delete(_testDownloadPath, true); + } + } + + [Fact] + public void ParseSongId_WithExternalId_ReturnsCorrectParts() + { + // Act + var (isExternal, provider, externalId) = _service.ParseSongId("ext-deezer-123456"); + + // Assert + Assert.True(isExternal); + Assert.Equal("deezer", provider); + Assert.Equal("123456", externalId); + } + + [Fact] + public void ParseSongId_WithLocalId_ReturnsNotExternal() + { + // Act + var (isExternal, provider, externalId) = _service.ParseSongId("local-789"); + + // Assert + Assert.False(isExternal); + Assert.Null(provider); + Assert.Null(externalId); + } + + [Fact] + public void ParseSongId_WithNumericId_ReturnsNotExternal() + { + // Act + var (isExternal, provider, externalId) = _service.ParseSongId("12345"); + + // Assert + Assert.False(isExternal); + Assert.Null(provider); + Assert.Null(externalId); + } + + [Fact] + public async Task GetLocalPathForExternalSongAsync_WhenNotRegistered_ReturnsNull() + { + // Act + var result = await _service.GetLocalPathForExternalSongAsync("deezer", "nonexistent"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task RegisterDownloadedSongAsync_ThenGetLocalPath_ReturnsPath() + { + // Arrange + var song = new Song + { + Id = "ext-deezer-123456", + Title = "Test Song", + Artist = "Test Artist", + Album = "Test Album", + ExternalProvider = "deezer", + ExternalId = "123456" + }; + var localPath = Path.Combine(_testDownloadPath, "test-song.mp3"); + + // Create the file + await File.WriteAllTextAsync(localPath, "fake audio content"); + + // Act + await _service.RegisterDownloadedSongAsync(song, localPath); + var result = await _service.GetLocalPathForExternalSongAsync("deezer", "123456"); + + // Assert + Assert.Equal(localPath, result); + } + + [Fact] + public async Task GetLocalPathForExternalSongAsync_WhenFileDeleted_ReturnsNull() + { + // Arrange + var song = new Song + { + Id = "ext-deezer-999999", + Title = "Deleted Song", + Artist = "Test Artist", + Album = "Test Album", + ExternalProvider = "deezer", + ExternalId = "999999" + }; + var localPath = Path.Combine(_testDownloadPath, "deleted-song.mp3"); + + // Create and then delete the file + await File.WriteAllTextAsync(localPath, "fake audio content"); + await _service.RegisterDownloadedSongAsync(song, localPath); + File.Delete(localPath); + + // Act + var result = await _service.GetLocalPathForExternalSongAsync("deezer", "999999"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task RegisterDownloadedSongAsync_WithNullProvider_DoesNothing() + { + // Arrange + var song = new Song + { + Id = "local-123", + Title = "Local Song", + Artist = "Local Artist", + Album = "Local Album", + ExternalProvider = null, + ExternalId = null + }; + var localPath = Path.Combine(_testDownloadPath, "local-song.mp3"); + + // Act - should not throw + await _service.RegisterDownloadedSongAsync(song, localPath); + + // Assert - nothing to assert, just checking it doesn't throw + Assert.True(true); + } +} diff --git a/octo-fiesta.Tests/octo-fiesta.Tests.csproj b/octo-fiesta.Tests/octo-fiesta.Tests.csproj new file mode 100644 index 0000000..dc92d35 --- /dev/null +++ b/octo-fiesta.Tests/octo-fiesta.Tests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + octo_fiesta.Tests + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/octo-fiesta.sln b/octo-fiesta.sln index e15e293..d16fc2f 100644 --- a/octo-fiesta.sln +++ b/octo-fiesta.sln @@ -1,16 +1,45 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "octo-fiesta", "octo-fiesta\octo-fiesta.csproj", "{C56EF44B-3AD5-4D4C-A513-726DA8A0E225}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "octo-fiesta", "octo-fiesta\octo-fiesta.csproj", "{C56EF44B-3AD5-4D4C-A513-726DA8A0E225}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "octo-fiesta.Tests", "octo-fiesta.Tests\octo-fiesta.Tests.csproj", "{72E3A16E-7020-4EE0-95D4-FB8FA027ED12}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x64.ActiveCfg = Debug|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x64.Build.0 = Debug|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x86.ActiveCfg = Debug|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x86.Build.0 = Debug|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|Any CPU.Build.0 = Release|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x64.ActiveCfg = Release|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x64.Build.0 = Release|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x86.ActiveCfg = Release|Any CPU + {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x86.Build.0 = Release|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x64.ActiveCfg = Debug|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x64.Build.0 = Debug|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x86.ActiveCfg = Debug|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x86.Build.0 = Debug|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|Any CPU.Build.0 = Release|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x64.ActiveCfg = Release|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x64.Build.0 = Release|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x86.ActiveCfg = Release|Any CPU + {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal