using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Moq; using NexusReader.Application.Features.Books.Commands; using NexusReader.Data.Persistence; using NexusReader.Domain.Entities; using Xunit; namespace NexusReader.Application.Tests.Commands; public class CreateBookTests : IDisposable { private readonly SqliteConnection _connection; private readonly DbContextOptions _contextOptions; private readonly Mock> _dbContextFactoryMock; public CreateBookTests() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); _contextOptions = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; // Seed initial database schema using var context = new AppDbContext(_contextOptions); context.Database.EnsureCreated(); _dbContextFactoryMock = new Mock>(); _dbContextFactoryMock.Setup(f => f.CreateDbContextAsync(It.IsAny())) .ReturnsAsync(() => new AppDbContext(_contextOptions)); } private NexusUser SeedUser(string userId, string tenantId) { var user = new NexusUser { Id = userId, UserName = $"user_{userId}", Email = $"{userId}@example.com", TenantId = tenantId, SubscriptionPlanId = 1 }; using var context = new AppDbContext(_contextOptions); context.Users.Add(user); context.SaveChanges(); return user; } [Fact] public async Task Handle_WithValidCommand_SuccessfullyCreatesBookRevisionAndIntroductionChapter() { // Arrange var userId = "creator-123"; var tenantId = "tenant-abc"; SeedUser(userId, tenantId); var command = new CreateBookCommand( Title: "The Art of Agentic Systems", Description: "A masterclass on building self-healing AI agents.", UserId: userId, TenantId: tenantId ); var handler = new CreateBookCommandHandler(_dbContextFactoryMock.Object); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Should().NotBeEmpty(); using (var context = new AppDbContext(_contextOptions)) { var book = await context.Books .Include(b => b.CurrentDraftRevision) .ThenInclude(r => r!.Chapters) .FirstOrDefaultAsync(b => b.Id == result.Value); book.Should().NotBeNull(); book!.Title.Should().Be("The Art of Agentic Systems"); book.UserId.Should().Be(userId); book.TenantId.Should().Be(tenantId); book.CurrentDraftRevisionId.Should().NotBeNull(); var revision = book.CurrentDraftRevision; revision.Should().NotBeNull(); revision!.VersionString.Should().Be("Working Draft"); revision.IsPublished.Should().BeFalse(); revision.BookId.Should().Be(book.Id); revision.Chapters.Should().HaveCount(1); var chapter = revision.Chapters.First(); chapter.Title.Should().Be("Introduction"); chapter.MarkdownContent.Should().Be("# Introduction\nStart writing here..."); chapter.SortOrder.Should().Be(1); } } [Fact] public async Task Handle_WithEmptyTitle_ReturnsFailureResult() { // Arrange var command = new CreateBookCommand( Title: "", Description: "No title", UserId: "user-1", TenantId: "tenant-1" ); var handler = new CreateBookCommandHandler(_dbContextFactoryMock.Object); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeFalse(); result.Errors.Should().NotBeEmpty(); result.Errors.First().Message.Should().Contain("title is required"); } [Fact] public async Task Handle_OnDatabaseViolation_RollsBackTransaction() { // Arrange // We trigger a database violation by not seeding the user 'missing-user' // and letting the foreign key constraint fail (if SQLite enforces it). // If foreign keys aren't strictly enforced on SQLite by default without PRAGMA, // we can check if it rolls back upon other violations, or manually verify error handling. var command = new CreateBookCommand( Title: "Violating Book", Description: "Triggering constraint failure", UserId: "non-existent-user-id-constraint", TenantId: "tenant-1" ); // Let's force foreign key constraints on SQLite to verify rollback using (var context = new AppDbContext(_contextOptions)) { context.Database.ExecuteSqlRaw("PRAGMA foreign_keys = ON;"); } var handler = new CreateBookCommandHandler(_dbContextFactoryMock.Object); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeFalse(); result.Errors.Should().NotBeEmpty(); // Ensure nothing was committed to the DB using (var context = new AppDbContext(_contextOptions)) { var books = await context.Books.ToListAsync(); books.Should().BeEmpty(); } } public void Dispose() { _connection.Close(); _connection.Dispose(); } }