Project scaffolding
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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"]);
|
||||
}
|
||||
|
||||
// ── 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<string>>(),
|
||||
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 = ["Frank Herbert"],
|
||||
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<string>>(), 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<string>>(a => a.Contains("Frank Herbert")),
|
||||
Arg.Is<(string, double, string?)?>(si => si != null && si.Value.Item1 == "Dune Chronicles"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user