Changed design language. Added editions, better support for authors. Base for file handling

This commit is contained in:
2026-03-28 15:17:20 +02:00
parent cbd7f52535
commit 5acde17a53
84 changed files with 5861 additions and 1983 deletions
@@ -175,6 +175,39 @@ public class BooksServiceTests
result.Authors.Select(a => a.Name).Should().BeEquivalentTo(["Alice", "Bob", "Carol"]);
}
// ── Editions DTO mapping ──────────────────────────────────────────────────
[Fact]
public async Task GetByIdAsync_BookWithEditions_MapsEditionsToDto()
{
var book = BookFactory.Create(id: 10)
.WithAuthors((1, "Author"))
.WithEditions(
("9780441013593", "Ace Books", "https://example.com/cover1.jpg"),
(null, "Bantam", null));
_repo.GetByIdAsync(10).Returns(book);
var result = await _sut.GetByIdAsync(10);
result!.Editions.Should().HaveCount(2);
result.Editions[0].Isbn.Should().Be("9780441013593");
result.Editions[0].Publisher.Should().Be("Ace Books");
result.Editions[0].CoverUrl.Should().Be("https://example.com/cover1.jpg");
result.Editions[1].Isbn.Should().BeNull();
result.Editions[1].Publisher.Should().Be("Bantam");
}
[Fact]
public async Task GetByIdAsync_BookWithNoEditions_ReturnsEmptyEditionsArray()
{
var book = BookFactory.Create(id: 11).WithAuthors((1, "Author"));
_repo.GetByIdAsync(11).Returns(book);
var result = await _sut.GetByIdAsync(11);
result!.Editions.Should().BeEmpty();
}
// ── CreateFromHardcoverAsync ──────────────────────────────────────────────
[Fact]
@@ -202,7 +235,7 @@ public class BooksServiceTests
result.Should().BeNull();
await _repo.DidNotReceive().CreateBookAsync(
Arg.Any<PageManager.Api.Data.Models.Book>(),
Arg.Any<IReadOnlyList<string>>(),
Arg.Any<IReadOnlyList<HardcoverAuthor>>(),
Arg.Any<(string, double, string?)?> ());
}
@@ -219,7 +252,7 @@ public class BooksServiceTests
Publisher = "Ace Books",
Pages = 412,
Description = "A sci-fi classic.",
Authors = ["Frank Herbert"],
Authors = [new HardcoverAuthor { Name = "Frank Herbert", Role = "Author" }],
Genres = ["Science Fiction"],
Isbn = "9780441013593",
CoverUrl = "https://example.com/cover.jpg",
@@ -231,7 +264,7 @@ public class BooksServiceTests
var createdBook = BookFactory.Create(id: 55, title: "Dune")
.WithAuthors((1, "Frank Herbert"))
.WithSeries(seriesName: "Dune Chronicles", position: 1.0);
_repo.CreateBookAsync(Arg.Any<PageManager.Api.Data.Models.Book>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<(string, double, string?)?>())
_repo.CreateBookAsync(Arg.Any<PageManager.Api.Data.Models.Book>(), Arg.Any<IReadOnlyList<HardcoverAuthor>>(), Arg.Any<(string, double, string?)?>())
.Returns(createdBook);
var result = await _sut.CreateFromHardcoverAsync(123);
@@ -242,7 +275,7 @@ public class BooksServiceTests
result.Series!.Name.Should().Be("Dune Chronicles");
await _repo.Received(1).CreateBookAsync(
Arg.Is<PageManager.Api.Data.Models.Book>(b => b.HardcoverId == 123 && b.Title == "Dune"),
Arg.Is<IReadOnlyList<string>>(a => a.Contains("Frank Herbert")),
Arg.Is<IReadOnlyList<HardcoverAuthor>>(a => a.Any(ha => ha.Name == "Frank Herbert")),
Arg.Is<(string, double, string?)?>(si => si != null && si.Value.Item1 == "Dune Chronicles"));
}
}
@@ -0,0 +1,90 @@
using FluentAssertions;
using PageManager.Api.Data.Models;
using PageManager.Api.Services;
using PageManager.Api.Tests.Helpers;
namespace PageManager.Api.Tests.Unit.Services;
public class FileScannerServiceTests
{
// ── GetFormatFromExtension ────────────────────────────────────────────────
[Theory]
[InlineData(".epub", FileFormat.Epub)]
[InlineData(".mobi", FileFormat.Mobi)]
[InlineData(".pdf", FileFormat.Pdf)]
[InlineData(".m4b", FileFormat.M4b)]
[InlineData(".mp3", FileFormat.Mp3)]
[InlineData(".aac", FileFormat.Aac)]
[InlineData(".flac", FileFormat.Flac)]
[InlineData(".EPUB", FileFormat.Epub)] // case-insensitive
[InlineData(".PDF", FileFormat.Pdf)]
public void GetFormatFromExtension_KnownExtension_ReturnsFormat(string ext, FileFormat expected)
{
FileScannerService.GetFormatFromExtension(ext).Should().Be(expected);
}
[Theory]
[InlineData(".txt")]
[InlineData(".docx")]
[InlineData(".zip")]
[InlineData("")]
public void GetFormatFromExtension_UnknownExtension_ReturnsNull(string ext)
{
FileScannerService.GetFormatFromExtension(ext).Should().BeNull();
}
// ── FindMatch ─────────────────────────────────────────────────────────────
[Fact]
public void FindMatch_FilenameContainsBookTitle_ReturnsBook()
{
var books = new[] { BookFactory.Create(id: 1, title: "Dune") };
var file = new BookFile { Filename = "Dune - Frank Herbert.epub" };
FileScannerService.FindMatch(file, books)!.Id.Should().Be(1);
}
[Fact]
public void FindMatch_CaseInsensitive_ReturnsBook()
{
var books = new[] { BookFactory.Create(id: 2, title: "The Way of Kings") };
var file = new BookFile { Filename = "the way of kings.epub" };
FileScannerService.FindMatch(file, books)!.Id.Should().Be(2);
}
[Fact]
public void FindMatch_FilenameContainsIsbn_ReturnsBook()
{
var books = new[] { BookFactory.Create(id: 3, title: "Foundation", isbn: "9780553293357") };
var file = new BookFile { Filename = "9780553293357.epub" };
FileScannerService.FindMatch(file, books)!.Id.Should().Be(3);
}
[Fact]
public void FindMatch_NoMatch_ReturnsNull()
{
var books = new[] { BookFactory.Create(id: 1, title: "Dune") };
var file = new BookFile { Filename = "Foundation - Isaac Asimov.epub" };
FileScannerService.FindMatch(file, books).Should().BeNull();
}
[Fact]
public void FindMatch_EmptyBookList_ReturnsNull()
{
FileScannerService.FindMatch(new BookFile { Filename = "anything.epub" }, []).Should().BeNull();
}
[Fact]
public void FindMatch_BookWithNullIsbn_DoesNotThrow()
{
var books = new[] { BookFactory.Create(id: 1, title: "Dune", isbn: null) };
var file = new BookFile { Filename = "some-file.epub" };
var act = () => FileScannerService.FindMatch(file, books);
act.Should().NotThrow();
}
}