using FluentAssertions; using PageManager.Api.Services; using PageManager.Api.Tests.Helpers; namespace PageManager.Api.Tests.Unit.Services; public class FileOrganizerServiceTests { // ── SanitizePathComponent ───────────────────────────────────────────────── [Theory] [InlineData("Andy Weir", "Andy Weir")] [InlineData("Brandon Sanderson", "Brandon Sanderson")] [InlineData("Hello: World", "Hello_ World")] // colon is invalid [InlineData("Title/Sub", "Title_Sub")] // slash is invalid [InlineData("Name.", "Name")] // trailing dot stripped [InlineData(" Name ", "Name")] // whitespace trimmed public void SanitizePathComponent_ReturnsExpected(string input, string expected) { FileOrganizerService.SanitizePathComponent(input).Should().Be(expected); } // ── ComputeCanonicalRelativePath — ebooks ───────────────────────────────── [Fact] public void Ebook_WithYear_ProducesAuthorTitleYearFolder() { var book = BookFactory.Create(title: "The Martian", year: 2011) .WithAuthors((1, "Andy Weir")); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false); result.Should().Be(Path.Combine("Andy Weir", "The Martian (2011)", "The Martian.epub")); } [Fact] public void Ebook_WithoutYear_OmitsYearFromFolder() { var book = BookFactory.Create(title: "Dune", year: null) .WithAuthors((1, "Frank Herbert")); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false); result.Should().Be(Path.Combine("Frank Herbert", "Dune", "Dune.epub")); } [Fact] public void Ebook_SeriesIgnored_SeriesNotInEbookPath() { var book = BookFactory.Create(title: "The Way of Kings", year: 2010) .WithAuthors((1, "Brandon Sanderson")) .WithSeries(seriesName: "The Stormlight Archive"); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false); // Series folder should NOT appear for ebooks result.Should().Be(Path.Combine("Brandon Sanderson", "The Way of Kings (2010)", "The Way of Kings.epub")); } // ── ComputeCanonicalRelativePath — audiobooks ───────────────────────────── [Fact] public void Audiobook_WithoutSeries_ProducesAuthorTitleTitle() { var book = BookFactory.Create(title: "Project Hail Mary", year: 2021) .WithAuthors((1, "Andy Weir")); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".m4b", isAudio: true); result.Should().Be(Path.Combine("Andy Weir", "Project Hail Mary", "Project Hail Mary.m4b")); } [Fact] public void Audiobook_WithSeries_InsertSeriesFolderBetweenAuthorAndTitle() { var book = BookFactory.Create(title: "The Way of Kings", year: 2010) .WithAuthors((1, "Brandon Sanderson")) .WithSeries(seriesName: "The Stormlight Archive"); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".m4b", isAudio: true); result.Should().Be(Path.Combine("Brandon Sanderson", "The Stormlight Archive", "The Way of Kings", "The Way of Kings.m4b")); } [Fact] public void Audiobook_Mp3Extension_PreservedInFilename() { var book = BookFactory.Create(title: "Dune", year: 1965) .WithAuthors((1, "Frank Herbert")); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".mp3", isAudio: true); result.Should().Be(Path.Combine("Frank Herbert", "Dune", "Dune.mp3")); } // ── Multiple authors ────────────────────────────────────────────────────── [Fact] public void MultipleAuthors_UsesPrimaryAuthorForPath() { var book = BookFactory.Create(title: "Good Omens", year: 1990) .WithAuthors((1, "Terry Pratchett"), (2, "Neil Gaiman")); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false); result.Should().StartWith("Terry Pratchett"); result.Should().NotContain("Neil Gaiman"); } [Fact] public void NoAuthors_UsesUnknownFolder() { var book = BookFactory.Create(title: "Anonymous Work", year: 2000); // No authors added var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false); result.Should().StartWith("Unknown"); } // ── Sanitization edge cases ─────────────────────────────────────────────── [Fact] public void TitleWithInvalidChars_Sanitized() { var book = BookFactory.Create(title: "Title: A Story", year: 2020) .WithAuthors((1, "Author Name")); var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false); result.Should().Be(Path.Combine("Author Name", "Title_ A Story (2020)", "Title_ A Story.epub")); } }