279 lines
10 KiB
C#
279 lines
10 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NSubstitute;
|
|
using PageManager.Api.Api.Dtos;
|
|
using PageManager.Api.Data;
|
|
using PageManager.Api.Data.Models;
|
|
using PageManager.Api.Services;
|
|
using PageManager.Api.Tests.Integration.Fixtures;
|
|
|
|
namespace PageManager.Api.Tests.Integration;
|
|
|
|
[Collection("Postgres")]
|
|
public class BooksControllerTests : IAsyncLifetime
|
|
{
|
|
private readonly TestWebAppFactory _factory;
|
|
private readonly HttpClient _client;
|
|
|
|
public BooksControllerTests(PostgresFixture postgres)
|
|
{
|
|
_factory = new TestWebAppFactory(postgres);
|
|
_client = _factory.CreateClient();
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await db.Database.ExecuteSqlRawAsync(
|
|
"TRUNCATE book_files, book_authors, series_entries, editions, books, authors, series RESTART IDENTITY CASCADE");
|
|
}
|
|
|
|
public Task DisposeAsync()
|
|
{
|
|
_client.Dispose();
|
|
_factory.Dispose();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── GET /api/books ────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetBooks_EmptyDb_Returns200WithEmptyArray()
|
|
{
|
|
var response = await _client.GetAsync("/api/books");
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var books = await response.Content.ReadFromJsonAsync<BookDto[]>();
|
|
books.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBooks_SeededBooks_Returns200WithCorrectCountAndArrayFields()
|
|
{
|
|
await SeedBookAsync(title: "Dune", formats: ["epub", "mobi"], genres: ["Science Fiction"]);
|
|
await SeedBookAsync(title: "Foundation", formats: ["pdf"], genres: ["Science Fiction", "Classic"]);
|
|
|
|
var response = await _client.GetAsync("/api/books");
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var books = await response.Content.ReadFromJsonAsync<BookDto[]>();
|
|
books.Should().HaveCount(2);
|
|
var dune = books!.Single(b => b.Title == "Dune");
|
|
dune.Formats.Should().BeEquivalentTo(new[] { "epub", "mobi" });
|
|
dune.Genres.Should().BeEquivalentTo(new[] { "Science Fiction" });
|
|
var foundation = books.Single(b => b.Title == "Foundation");
|
|
foundation.Formats.Should().BeEquivalentTo(new[] { "pdf" });
|
|
}
|
|
|
|
// ── GET /api/books/{id} ───────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetBook_Exists_Returns200WithDtoAndAuthors()
|
|
{
|
|
var id = await SeedBookAsync(title: "The Name of the Wind", authorName: "Patrick Rothfuss");
|
|
|
|
var response = await _client.GetAsync($"/api/books/{id}");
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var book = await response.Content.ReadFromJsonAsync<BookDto>();
|
|
book.Should().NotBeNull();
|
|
book!.Title.Should().Be("The Name of the Wind");
|
|
book.Authors.Should().ContainSingle(a => a.Name == "Patrick Rothfuss");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBook_NotFound_Returns404()
|
|
{
|
|
var response = await _client.GetAsync("/api/books/99999");
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
// ── PUT /api/books/{id} ───────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task UpdateBook_Exists_Returns200WithUpdatedFields()
|
|
{
|
|
var id = await SeedBookAsync(title: "Original Title");
|
|
|
|
var req = new UpdateBookRequest
|
|
{
|
|
Title = "Updated Title",
|
|
Year = 2025,
|
|
Publisher = "New Publisher",
|
|
Pages = 500,
|
|
Formats = ["epub", "mobi"],
|
|
Genres = ["Fantasy"],
|
|
Color = "#ff0000",
|
|
};
|
|
|
|
var response = await _client.PutAsJsonAsync($"/api/books/{id}", req);
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var book = await response.Content.ReadFromJsonAsync<BookDto>();
|
|
book!.Title.Should().Be("Updated Title");
|
|
book.Year.Should().Be(2025);
|
|
book.Publisher.Should().Be("New Publisher");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBook_NotFound_Returns404()
|
|
{
|
|
var req = new UpdateBookRequest { Title = "Whatever" };
|
|
|
|
var response = await _client.PutAsJsonAsync("/api/books/99999", req);
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBook_PersistsArrayFieldsToDb()
|
|
{
|
|
var id = await SeedBookAsync(title: "Array Test");
|
|
|
|
var req = new UpdateBookRequest
|
|
{
|
|
Title = "Array Test",
|
|
Formats = ["epub", "pdf", "mobi"],
|
|
Genres = ["Fantasy", "Adventure"],
|
|
Color = "#6366f1",
|
|
};
|
|
|
|
await _client.PutAsJsonAsync($"/api/books/{id}", req);
|
|
|
|
using var scope = _factory.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var book = await db.Books.FindAsync(id);
|
|
book!.Formats.Should().BeEquivalentTo(["epub", "pdf", "mobi"]);
|
|
book.Genres.Should().BeEquivalentTo(["Fantasy", "Adventure"]);
|
|
}
|
|
|
|
// ── POST /api/books ───────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task CreateBook_HardcoverReturnsDetails_Returns200WithCreatedBook()
|
|
{
|
|
var mockHardcover = Substitute.For<IHardcoverService>();
|
|
mockHardcover.GetBookDetailsAsync(999).Returns(new HardcoverBookDetails
|
|
{
|
|
Id = 999,
|
|
Title = "Dune",
|
|
Year = 1965,
|
|
Publisher = "Ace Books",
|
|
Pages = 412,
|
|
Authors = [new HardcoverAuthor { Name = "Frank Herbert", Role = "Author" }],
|
|
Genres = ["Science Fiction"],
|
|
Isbn = "9780441013593",
|
|
CoverColor = "#c4a35a",
|
|
});
|
|
|
|
using var client = _factory
|
|
.WithWebHostBuilder(b => b.ConfigureServices(services =>
|
|
{
|
|
services.Remove(services.Single(d => d.ServiceType == typeof(IHardcoverService)));
|
|
services.AddScoped(_ => mockHardcover);
|
|
}))
|
|
.CreateClient();
|
|
|
|
var response = await client.PostAsJsonAsync("/api/books", new { hardcoverId = 999 });
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var book = await response.Content.ReadFromJsonAsync<BookDto>();
|
|
book.Should().NotBeNull();
|
|
book!.Title.Should().Be("Dune");
|
|
book.HardcoverId.Should().Be(999);
|
|
book.Authors.Should().ContainSingle(a => a.Name == "Frank Herbert");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateBook_HardcoverReturnsNull_Returns404()
|
|
{
|
|
var mockHardcover = Substitute.For<IHardcoverService>();
|
|
mockHardcover.GetBookDetailsAsync(Arg.Any<int>()).Returns((HardcoverBookDetails?)null);
|
|
|
|
using var client = _factory
|
|
.WithWebHostBuilder(b => b.ConfigureServices(services =>
|
|
{
|
|
services.Remove(services.Single(d => d.ServiceType == typeof(IHardcoverService)));
|
|
services.AddScoped(_ => mockHardcover);
|
|
}))
|
|
.CreateClient();
|
|
|
|
var response = await client.PostAsJsonAsync("/api/books", new { hardcoverId = 0 });
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateBook_HardcoverIdAlreadyInDb_Returns200WithExistingBook()
|
|
{
|
|
await SeedBookWithHardcoverIdAsync(hardcoverId: 777, title: "Already Here");
|
|
|
|
var mockHardcover = Substitute.For<IHardcoverService>();
|
|
|
|
using var client = _factory
|
|
.WithWebHostBuilder(b => b.ConfigureServices(services =>
|
|
{
|
|
services.Remove(services.Single(d => d.ServiceType == typeof(IHardcoverService)));
|
|
services.AddScoped(_ => mockHardcover);
|
|
}))
|
|
.CreateClient();
|
|
|
|
var response = await client.PostAsJsonAsync("/api/books", new { hardcoverId = 777 });
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var book = await response.Content.ReadFromJsonAsync<BookDto>();
|
|
book!.Title.Should().Be("Already Here");
|
|
await mockHardcover.DidNotReceive().GetBookDetailsAsync(Arg.Any<int>());
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
private async Task<int> SeedBookAsync(
|
|
string title = "Test Book",
|
|
string[] formats = null!,
|
|
string[] genres = null!,
|
|
string? authorName = null)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
|
|
var book = new Book
|
|
{
|
|
Title = title,
|
|
Formats = formats ?? [],
|
|
Genres = genres ?? [],
|
|
};
|
|
db.Books.Add(book);
|
|
|
|
if (authorName is not null)
|
|
{
|
|
var author = new Author { Name = authorName };
|
|
db.Authors.Add(author);
|
|
await db.SaveChangesAsync();
|
|
|
|
db.BookAuthors.Add(new BookAuthor
|
|
{
|
|
BookId = book.Id,
|
|
AuthorId = author.Id,
|
|
});
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
return book.Id;
|
|
}
|
|
|
|
private async Task SeedBookWithHardcoverIdAsync(int hardcoverId, string title)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
db.Books.Add(new Book { Title = title, HardcoverId = hardcoverId, Formats = [], Genres = [] });
|
|
await db.SaveChangesAsync();
|
|
}
|
|
}
|