282 lines
11 KiB
C#
282 lines
11 KiB
C#
using FluentAssertions;
|
|
using NSubstitute;
|
|
using PageManager.Api.Api.Dtos;
|
|
using PageManager.Api.Data.Repositories;
|
|
using PageManager.Api.Services;
|
|
using PageManager.Api.Tests.Helpers;
|
|
|
|
namespace PageManager.Api.Tests.Unit.Services;
|
|
|
|
public class BooksServiceTests
|
|
{
|
|
private readonly IBooksRepository _repo = Substitute.For<IBooksRepository>();
|
|
private readonly IHardcoverService _hardcover = Substitute.For<IHardcoverService>();
|
|
private readonly BooksService _sut;
|
|
|
|
public BooksServiceTests()
|
|
{
|
|
_sut = new BooksService(_repo, _hardcover);
|
|
}
|
|
|
|
// ── GetAllAsync ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_MultipleBooks_ReturnsMappedDtos()
|
|
{
|
|
var books = new[]
|
|
{
|
|
BookFactory.Create(id: 1, title: "Book One").WithAuthors((1, "Author A")),
|
|
BookFactory.Create(id: 2, title: "Book Two").WithAuthors((2, "Author B")),
|
|
};
|
|
_repo.GetAllAsync().Returns(books);
|
|
|
|
var result = (await _sut.GetAllAsync()).ToArray();
|
|
|
|
result.Should().HaveCount(2);
|
|
result[0].Title.Should().Be("Book One");
|
|
result[1].Title.Should().Be("Book Two");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_EmptyRepo_ReturnsEmptyCollection()
|
|
{
|
|
_repo.GetAllAsync().Returns([]);
|
|
|
|
var result = await _sut.GetAllAsync();
|
|
|
|
result.Should().BeEmpty();
|
|
}
|
|
|
|
// ── GetByIdAsync ──────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_BookExists_ReturnsDtoWithCorrectFields()
|
|
{
|
|
var book = BookFactory.Create(id: 42, title: "Found Book", year: 2020, publisher: "Pub", pages: 100)
|
|
.WithAuthors((7, "Jane Doe"))
|
|
.WithSeries(seriesName: "Great Series", position: 2.0, arc: "Part One");
|
|
_repo.GetByIdAsync(42).Returns(book);
|
|
|
|
var result = await _sut.GetByIdAsync(42);
|
|
|
|
result.Should().NotBeNull();
|
|
result!.Id.Should().Be(42);
|
|
result.Title.Should().Be("Found Book");
|
|
result.Year.Should().Be(2020);
|
|
result.Publisher.Should().Be("Pub");
|
|
result.Pages.Should().Be(100);
|
|
result.Authors.Should().ContainSingle(a => a.Id == 7 && a.Name == "Jane Doe");
|
|
result.Series.Should().NotBeNull();
|
|
result.Series!.Name.Should().Be("Great Series");
|
|
result.Series.Position.Should().Be(2.0);
|
|
result.Series.Arc.Should().Be("Part One");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_BookNotFound_ReturnsNull()
|
|
{
|
|
_repo.GetByIdAsync(99).Returns((PageManager.Api.Data.Models.Book?)null);
|
|
|
|
var result = await _sut.GetByIdAsync(99);
|
|
|
|
result.Should().BeNull();
|
|
}
|
|
|
|
// ── UpdateAsync ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_BookExists_AppliesAllFieldsAndSaves()
|
|
{
|
|
var book = BookFactory.Create(id: 5).WithAuthors((1, "Author"));
|
|
_repo.GetByIdAsync(5).Returns(book);
|
|
|
|
var req = new UpdateBookRequest
|
|
{
|
|
Title = "Updated Title",
|
|
Year = 2025,
|
|
Publisher = "New Publisher",
|
|
Pages = 999,
|
|
Description = "New description",
|
|
Formats = ["epub", "mobi"],
|
|
Color = "#ff0000",
|
|
Genres = ["Fantasy", "Adventure"],
|
|
CoverUrl = "https://example.com/cover.jpg",
|
|
Isbn = "978-0-00-000000-0",
|
|
HardcoverId = 123,
|
|
};
|
|
|
|
var result = await _sut.UpdateAsync(5, req);
|
|
|
|
result.Should().NotBeNull();
|
|
result!.Title.Should().Be("Updated Title");
|
|
result.Year.Should().Be(2025);
|
|
result.Publisher.Should().Be("New Publisher");
|
|
result.Pages.Should().Be(999);
|
|
result.Description.Should().Be("New description");
|
|
result.Formats.Should().BeEquivalentTo(["epub", "mobi"]);
|
|
result.Color.Should().Be("#ff0000");
|
|
result.Genres.Should().BeEquivalentTo(["Fantasy", "Adventure"]);
|
|
result.CoverUrl.Should().Be("https://example.com/cover.jpg");
|
|
result.Isbn.Should().Be("978-0-00-000000-0");
|
|
result.HardcoverId.Should().Be(123);
|
|
await _repo.Received(1).SaveAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_BookNotFound_ReturnsNullWithoutSaving()
|
|
{
|
|
_repo.GetByIdAsync(99).Returns((PageManager.Api.Data.Models.Book?)null);
|
|
|
|
var result = await _sut.UpdateAsync(99, new UpdateBookRequest());
|
|
|
|
result.Should().BeNull();
|
|
await _repo.DidNotReceive().SaveAsync();
|
|
}
|
|
|
|
// ── DTO mapping ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_BookWithSeries_MapsSeriesNamePositionArc()
|
|
{
|
|
var book = BookFactory.Create(id: 1)
|
|
.WithAuthors((1, "Author"))
|
|
.WithSeries(seriesId: 10, seriesName: "The Stormlight Archive", position: 1.0, arc: "Book One");
|
|
_repo.GetByIdAsync(1).Returns(book);
|
|
|
|
var result = await _sut.GetByIdAsync(1);
|
|
|
|
result!.Series.Should().NotBeNull();
|
|
result.Series!.Name.Should().Be("The Stormlight Archive");
|
|
result.Series.Position.Should().Be(1.0);
|
|
result.Series.Arc.Should().Be("Book One");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_BookWithoutSeries_SeriesIsNull()
|
|
{
|
|
var book = BookFactory.Create(id: 2).WithAuthors((1, "Author"));
|
|
_repo.GetByIdAsync(2).Returns(book);
|
|
|
|
var result = await _sut.GetByIdAsync(2);
|
|
|
|
result!.Series.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_BookWithMultipleAuthors_MapsAllAuthors()
|
|
{
|
|
var book = BookFactory.Create(id: 3)
|
|
.WithAuthors((1, "Alice"), (2, "Bob"), (3, "Carol"));
|
|
_repo.GetByIdAsync(3).Returns(book);
|
|
|
|
var result = await _sut.GetByIdAsync(3);
|
|
|
|
result!.Authors.Should().HaveCount(3);
|
|
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]
|
|
public async Task CreateFromHardcoverAsync_AlreadyInDb_ReturnsExistingWithoutCallingHardcover()
|
|
{
|
|
var existing = BookFactory.Create(id: 7, title: "Existing").WithAuthors((1, "Author"));
|
|
_repo.FindByHardcoverIdAsync(42).Returns(existing);
|
|
|
|
var result = await _sut.CreateFromHardcoverAsync(42);
|
|
|
|
result.Should().NotBeNull();
|
|
result!.Id.Should().Be(7);
|
|
result.Title.Should().Be("Existing");
|
|
await _hardcover.DidNotReceive().GetBookDetailsAsync(Arg.Any<int>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFromHardcoverAsync_HardcoverReturnsNull_ReturnsNull()
|
|
{
|
|
_repo.FindByHardcoverIdAsync(99).Returns((PageManager.Api.Data.Models.Book?)null);
|
|
_hardcover.GetBookDetailsAsync(99).Returns((HardcoverBookDetails?)null);
|
|
|
|
var result = await _sut.CreateFromHardcoverAsync(99);
|
|
|
|
result.Should().BeNull();
|
|
await _repo.DidNotReceive().CreateBookAsync(
|
|
Arg.Any<PageManager.Api.Data.Models.Book>(),
|
|
Arg.Any<IReadOnlyList<HardcoverAuthor>>(),
|
|
Arg.Any<(string, double, string?)?> ());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFromHardcoverAsync_Success_CreatesBookAndReturnsDto()
|
|
{
|
|
_repo.FindByHardcoverIdAsync(123).Returns((PageManager.Api.Data.Models.Book?)null);
|
|
|
|
var details = new HardcoverBookDetails
|
|
{
|
|
Id = 123,
|
|
Title = "Dune",
|
|
Year = 1965,
|
|
Publisher = "Ace Books",
|
|
Pages = 412,
|
|
Description = "A sci-fi classic.",
|
|
Authors = [new HardcoverAuthor { Name = "Frank Herbert", Role = "Author" }],
|
|
Genres = ["Science Fiction"],
|
|
Isbn = "9780441013593",
|
|
CoverUrl = "https://example.com/cover.jpg",
|
|
CoverColor = "#c4a35a",
|
|
Series = new HardcoverSeriesInfo { Name = "Dune Chronicles", Position = 1.0 },
|
|
};
|
|
_hardcover.GetBookDetailsAsync(123).Returns(details);
|
|
|
|
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<HardcoverAuthor>>(), Arg.Any<(string, double, string?)?>())
|
|
.Returns(createdBook);
|
|
|
|
var result = await _sut.CreateFromHardcoverAsync(123);
|
|
|
|
result.Should().NotBeNull();
|
|
result!.Title.Should().Be("Dune");
|
|
result.Series.Should().NotBeNull();
|
|
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<HardcoverAuthor>>(a => a.Any(ha => ha.Name == "Frank Herbert")),
|
|
Arg.Is<(string, double, string?)?>(si => si != null && si.Value.Item1 == "Dune Chronicles"));
|
|
}
|
|
}
|