using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Moq; using NexusReader.Application.Queries.Creator; using NexusReader.Data.Persistence; using NexusReader.Domain.Entities; using NexusReader.Domain.Exceptions; using Xunit; namespace NexusReader.Application.Tests.Queries; public class CreatorDashboardTests : IDisposable { private readonly SqliteConnection _connection; private readonly DbContextOptions _contextOptions; private readonly Mock> _dbContextFactoryMock; public CreatorDashboardTests() { _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 CreateTestUser(string userId, string tenantId) { return new NexusUser { Id = userId, UserName = $"user_{userId}", Email = $"{userId}@example.com", TenantId = tenantId, SubscriptionPlanId = 1 }; } [Fact] public async Task GetCreatorDashboardData_WithValidUser_ProjectsCorrectlyAndNeverLoadsMarkdownToTracker() { // Arrange var userId = "creator-123"; var tenantId = "tenant-abc"; var bookId = Guid.NewGuid(); var user = CreateTestUser(userId, tenantId); var book = new Book { Id = bookId, Title = "Authored Masterpiece", UserId = userId, TenantId = tenantId }; var draft = new BookRevision { Id = Guid.NewGuid(), BookId = bookId, VersionString = "Working Draft", IsPublished = false, CreatedAt = DateTime.UtcNow }; // Standard markdown content (length 58 characters -> estimated word count: 9 words) var chapter = new Chapter { Id = Guid.NewGuid(), BookRevisionId = draft.Id, Title = "Chapter One", MarkdownContent = "This is a content snippet that contains exactly ten words.", // 58 chars SortOrder = 1 }; using (var context = new AppDbContext(_contextOptions)) { context.Users.Add(user); context.Books.Add(book); context.BookRevisions.Add(draft); context.Chapters.Add(chapter); await context.SaveChangesAsync(); // Link draft revision var dbBook = await context.Books.FindAsync(bookId); dbBook!.CurrentDraftRevisionId = draft.Id; await context.SaveChangesAsync(); } var query = new GetCreatorDashboardDataQuery(userId, tenantId); var handler = new GetCreatorDashboardDataQueryHandler(_dbContextFactoryMock.Object); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Should().NotBeNull(); result.Value.Books.Should().HaveCount(1); var bookDto = result.Value.Books.First(); bookDto.Title.Should().Be("Authored Masterpiece"); bookDto.WordCount.Should().Be(58 / 6); // projected word count calculation check bookDto.AggregatedReads.Should().Be(Math.Abs(bookId.GetHashCode() % 1000) + 120); // Verify metrics are calculated result.Value.Metrics.TotalReads.Should().Be(bookDto.AggregatedReads); result.Value.Metrics.ActiveReaders.Should().BeGreaterThan(0); result.Value.Metrics.GrossRevenue.Should().Be(bookDto.AggregatedReads * 1.49m); result.Value.Metrics.AvgReadTimeMinutes.Should().Be(Math.Round((58 / 6) / 250.0, 1)); } [Fact] public async Task GetCreatorDashboardData_EnforcesTenantAndUserBoundaries() { // Arrange var userId = "creator-123"; var tenantId = "tenant-abc"; var bookId = Guid.NewGuid(); var user = CreateTestUser(userId, tenantId); var book = new Book { Id = bookId, Title = "Authored Masterpiece", UserId = userId, TenantId = tenantId }; using (var context = new AppDbContext(_contextOptions)) { context.Users.Add(user); context.Books.Add(book); await context.SaveChangesAsync(); } // Query with mismatched tenant ID var queryMismatchedTenant = new GetCreatorDashboardDataQuery(userId, "different-tenant"); var handler = new GetCreatorDashboardDataQueryHandler(_dbContextFactoryMock.Object); // Act var resultMismatchedTenant = await handler.Handle(queryMismatchedTenant, CancellationToken.None); // Assert resultMismatchedTenant.IsSuccess.Should().BeTrue(); resultMismatchedTenant.Value.Books.Should().BeEmpty(); resultMismatchedTenant.Value.Metrics.TotalReads.Should().Be(0); // Query with mismatched user ID var queryMismatchedUser = new GetCreatorDashboardDataQuery("different-user", tenantId); // Act var resultMismatchedUser = await handler.Handle(queryMismatchedUser, CancellationToken.None); // Assert resultMismatchedUser.IsSuccess.Should().BeTrue(); resultMismatchedUser.Value.Books.Should().BeEmpty(); } [Fact] public async Task GetBookRevisions_WithValidBook_ReturnsRevisionsOrderedByDate() { // Arrange var userId = "creator-123"; var tenantId = "tenant-abc"; var bookId = Guid.NewGuid(); var user = CreateTestUser(userId, tenantId); var book = new Book { Id = bookId, Title = "Authored Masterpiece", UserId = userId, TenantId = tenantId }; var revision1 = new BookRevision { Id = Guid.NewGuid(), BookId = bookId, VersionString = "v1.0.0", IsPublished = true, CreatedAt = DateTime.UtcNow.AddMinutes(-5), PublishedAt = DateTime.UtcNow.AddMinutes(-5) }; var revision2 = new BookRevision { Id = Guid.NewGuid(), BookId = bookId, VersionString = "Working Draft", IsPublished = false, CreatedAt = DateTime.UtcNow }; using (var context = new AppDbContext(_contextOptions)) { context.Users.Add(user); context.Books.Add(book); context.BookRevisions.Add(revision1); context.BookRevisions.Add(revision2); await context.SaveChangesAsync(); } var query = new GetBookRevisionsQuery(bookId, userId, tenantId); var handler = new GetBookRevisionsQueryHandler(_dbContextFactoryMock.Object); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Should().HaveCount(2); // Ordered by CreatedAt descending result.Value[0].VersionString.Should().Be("Working Draft"); result.Value[1].VersionString.Should().Be("v1.0.0"); } [Fact] public async Task GetBookRevisions_WithMismatchedUserOrTenant_ThrowsBookNotFoundException() { // Arrange var userId = "creator-123"; var tenantId = "tenant-abc"; var bookId = Guid.NewGuid(); var user = CreateTestUser(userId, tenantId); var book = new Book { Id = bookId, Title = "Authored Masterpiece", UserId = userId, TenantId = tenantId }; using (var context = new AppDbContext(_contextOptions)) { context.Users.Add(user); context.Books.Add(book); await context.SaveChangesAsync(); } var handler = new GetBookRevisionsQueryHandler(_dbContextFactoryMock.Object); // Act & Assert var queryMismatchedTenant = new GetBookRevisionsQuery(bookId, userId, "different-tenant"); var actionTenant = () => handler.Handle(queryMismatchedTenant, CancellationToken.None); await actionTenant.Should().ThrowAsync(); var queryMismatchedUser = new GetBookRevisionsQuery(bookId, "different-user", tenantId); var actionUser = () => handler.Handle(queryMismatchedUser, CancellationToken.None); await actionUser.Should().ThrowAsync(); } public void Dispose() { _connection.Close(); _connection.Dispose(); } }