Files
PageManager/PageManager.Api/PageManager.Api.Tests/Unit/Services/BooksServiceTests.cs
T

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"));
}
}