Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b74ba4ba54 | |||
| 4aa3b4b421 | |||
| 431d815f55 | |||
| 08905a248d | |||
| a738a28eb4 | |||
| d2410e9793 | |||
| bb861e469f | |||
| ecabe01be0 | |||
| 00ebee8628 | |||
| 042dad0774 | |||
| 893fed4d60 | |||
| 8856fb1614 | |||
| 978485e8ff | |||
| 155bfa9aa0 |
+1
-1
@@ -38,7 +38,7 @@ public class PublishBookVersionCommandHandler : ICommandHandler<PublishBookVersi
|
|||||||
|
|
||||||
if (book == null)
|
if (book == null)
|
||||||
{
|
{
|
||||||
return Result.Fail(new Error($"Book with ID '{request.BookId}' was not found."));
|
throw new BookNotFoundException(request.BookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldDraftRevision = book.CurrentDraftRevision;
|
var oldDraftRevision = book.CurrentDraftRevision;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class GetBookRevisionsQueryHandler : IQueryHandler<GetBookRevisionsQuery,
|
|||||||
|
|
||||||
if (!bookExists)
|
if (!bookExists)
|
||||||
{
|
{
|
||||||
return FluentResults.Result.Fail<List<CreatorBookRevisionDto>>(new FluentResults.Error($"Book with ID '{request.BookId}' was not found."));
|
throw new BookNotFoundException(request.BookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all revisions sorted chronologically
|
// Fetch all revisions sorted chronologically
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
private IJSObjectReference? _module;
|
private IJSObjectReference? _module;
|
||||||
private DotNetObjectReference<MarkdownEditor>? _dotNetHelper;
|
private DotNetObjectReference<MarkdownEditor>? _dotNetHelper;
|
||||||
private string? _lastInitializedEditorId;
|
private string? _lastInitializedEditorId;
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
private enum SaveStatus
|
private enum SaveStatus
|
||||||
{
|
{
|
||||||
@@ -351,7 +350,6 @@
|
|||||||
|
|
||||||
// Cancel pending timers thread-safely
|
// Cancel pending timers thread-safely
|
||||||
CancellationTokenSource? ctsToCancel = null;
|
CancellationTokenSource? ctsToCancel = null;
|
||||||
CancellationToken token;
|
|
||||||
lock (_timerLock)
|
lock (_timerLock)
|
||||||
{
|
{
|
||||||
if (_debounceCts != null)
|
if (_debounceCts != null)
|
||||||
@@ -360,7 +358,6 @@
|
|||||||
_debounceCts = null;
|
_debounceCts = null;
|
||||||
}
|
}
|
||||||
_debounceCts = new CancellationTokenSource();
|
_debounceCts = new CancellationTokenSource();
|
||||||
token = _debounceCts.Token; // Capture token synchronously under lock on UI thread
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctsToCancel != null)
|
if (ctsToCancel != null)
|
||||||
@@ -379,6 +376,13 @@
|
|||||||
// Start 5-second idle debounce timer
|
// Start 5-second idle debounce timer
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
CancellationToken token;
|
||||||
|
lock (_timerLock)
|
||||||
|
{
|
||||||
|
if (_debounceCts == null) return;
|
||||||
|
token = _debounceCts.Token;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(5000, token);
|
await Task.Delay(5000, token);
|
||||||
@@ -397,7 +401,7 @@
|
|||||||
|
|
||||||
private async Task TriggerAutosaveAsync(string markdown, CancellationToken token)
|
private async Task TriggerAutosaveAsync(string markdown, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested || _disposed) return;
|
if (token.IsCancellationRequested) return;
|
||||||
|
|
||||||
_status = SaveStatus.Saving;
|
_status = SaveStatus.Saving;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
@@ -412,8 +416,6 @@
|
|||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// Purge LocalStorage backup key on HTTP success
|
// Purge LocalStorage backup key on HTTP success
|
||||||
@@ -429,12 +431,10 @@
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
|
||||||
_status = SaveStatus.OfflineLocalBackup;
|
_status = SaveStatus.OfflineLocalBackup;
|
||||||
Console.WriteLine($"[MarkdownEditor] Autosave HTTP exception: {ex.Message}");
|
Console.WriteLine($"[MarkdownEditor] Autosave HTTP exception: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_disposed) return;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +477,6 @@
|
|||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
_disposed = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
|
|||||||
@@ -519,15 +519,18 @@ app.MapGet("/api/creator/books/{bookId:guid}/revisions", async (Guid bookId, Cla
|
|||||||
|
|
||||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var result = await mediator.Send(new NexusReader.Application.Queries.Creator.GetBookRevisionsQuery(bookId, userId, tenantId));
|
var result = await mediator.Send(new NexusReader.Application.Queries.Creator.GetBookRevisionsQuery(bookId, userId, tenantId));
|
||||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||||
|
|
||||||
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
||||||
if (errorMsg.Contains("was not found", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return Results.NotFound(errorMsg);
|
|
||||||
}
|
|
||||||
return Results.BadRequest(errorMsg);
|
return Results.BadRequest(errorMsg);
|
||||||
|
}
|
||||||
|
catch (NexusReader.Domain.Exceptions.BookNotFoundException)
|
||||||
|
{
|
||||||
|
return Results.NotFound($"Book with ID '{bookId}' was not found.");
|
||||||
|
}
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapPost("/api/creator/books/{bookId:guid}/publish", async (Guid bookId, [FromQuery] string version, ClaimsPrincipal user, IMediator mediator) =>
|
app.MapPost("/api/creator/books/{bookId:guid}/publish", async (Guid bookId, [FromQuery] string version, ClaimsPrincipal user, IMediator mediator) =>
|
||||||
@@ -542,15 +545,18 @@ app.MapPost("/api/creator/books/{bookId:guid}/publish", async (Guid bookId, [Fro
|
|||||||
return Results.BadRequest("Version string is required.");
|
return Results.BadRequest("Version string is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var result = await mediator.Send(new NexusReader.Application.Features.Books.Commands.PublishBookVersionCommand(bookId, version, userId, tenantId));
|
var result = await mediator.Send(new NexusReader.Application.Features.Books.Commands.PublishBookVersionCommand(bookId, version, userId, tenantId));
|
||||||
if (result.IsSuccess) return Results.Ok();
|
if (result.IsSuccess) return Results.Ok();
|
||||||
|
|
||||||
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
||||||
if (errorMsg.Contains("was not found", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return Results.NotFound(errorMsg);
|
|
||||||
}
|
|
||||||
return Results.BadRequest(errorMsg);
|
return Results.BadRequest(errorMsg);
|
||||||
|
}
|
||||||
|
catch (NexusReader.Domain.Exceptions.BookNotFoundException)
|
||||||
|
{
|
||||||
|
return Results.NotFound($"Book with ID '{bookId}' was not found.");
|
||||||
|
}
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapPost("/api/creator/books", async (
|
app.MapPost("/api/creator/books", async (
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ public class PublishBookVersionTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WithMismatchedTenantId_ReturnsFailure()
|
public async Task Handle_WithMismatchedTenantId_ThrowsBookNotFoundException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var bookId = Guid.NewGuid();
|
var bookId = Guid.NewGuid();
|
||||||
@@ -210,16 +210,13 @@ public class PublishBookVersionTests : IDisposable
|
|||||||
|
|
||||||
var handler = new PublishBookVersionCommandHandler(_dbContextFactoryMock.Object);
|
var handler = new PublishBookVersionCommandHandler(_dbContextFactoryMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var result = await handler.Handle(command, CancellationToken.None);
|
var action = () => handler.Handle(command, CancellationToken.None);
|
||||||
|
await action.Should().ThrowAsync<BookNotFoundException>();
|
||||||
// Assert
|
|
||||||
result.IsSuccess.Should().BeFalse();
|
|
||||||
result.Errors.Should().Contain(e => e.Message.Contains("was not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WithMismatchedUserId_ReturnsFailure()
|
public async Task Handle_WithMismatchedUserId_ThrowsBookNotFoundException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var bookId = Guid.NewGuid();
|
var bookId = Guid.NewGuid();
|
||||||
@@ -260,16 +257,13 @@ public class PublishBookVersionTests : IDisposable
|
|||||||
|
|
||||||
var handler = new PublishBookVersionCommandHandler(_dbContextFactoryMock.Object);
|
var handler = new PublishBookVersionCommandHandler(_dbContextFactoryMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var result = await handler.Handle(command, CancellationToken.None);
|
var action = () => handler.Handle(command, CancellationToken.None);
|
||||||
|
await action.Should().ThrowAsync<BookNotFoundException>();
|
||||||
// Assert
|
|
||||||
result.IsSuccess.Should().BeFalse();
|
|
||||||
result.Errors.Should().Contain(e => e.Message.Contains("was not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WithNonExistentBook_ReturnsFailure()
|
public async Task Handle_WithNonExistentBook_ThrowsBookNotFoundException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var command = new PublishBookVersionCommand(
|
var command = new PublishBookVersionCommand(
|
||||||
@@ -281,12 +275,9 @@ public class PublishBookVersionTests : IDisposable
|
|||||||
|
|
||||||
var handler = new PublishBookVersionCommandHandler(_dbContextFactoryMock.Object);
|
var handler = new PublishBookVersionCommandHandler(_dbContextFactoryMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var result = await handler.Handle(command, CancellationToken.None);
|
var action = () => handler.Handle(command, CancellationToken.None);
|
||||||
|
await action.Should().ThrowAsync<BookNotFoundException>();
|
||||||
// Assert
|
|
||||||
result.IsSuccess.Should().BeFalse();
|
|
||||||
result.Errors.Should().Contain(e => e.Message.Contains("was not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ public class CreatorDashboardTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBookRevisions_WithMismatchedUserOrTenant_ReturnsFailure()
|
public async Task GetBookRevisions_WithMismatchedUserOrTenant_ThrowsBookNotFoundException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = "creator-123";
|
var userId = "creator-123";
|
||||||
@@ -262,14 +262,12 @@ public class CreatorDashboardTests : IDisposable
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var queryMismatchedTenant = new GetBookRevisionsQuery(bookId, userId, "different-tenant");
|
var queryMismatchedTenant = new GetBookRevisionsQuery(bookId, userId, "different-tenant");
|
||||||
var resultTenant = await handler.Handle(queryMismatchedTenant, CancellationToken.None);
|
var actionTenant = () => handler.Handle(queryMismatchedTenant, CancellationToken.None);
|
||||||
resultTenant.IsSuccess.Should().BeFalse();
|
await actionTenant.Should().ThrowAsync<BookNotFoundException>();
|
||||||
resultTenant.Errors.Should().Contain(e => e.Message.Contains("was not found"));
|
|
||||||
|
|
||||||
var queryMismatchedUser = new GetBookRevisionsQuery(bookId, "different-user", tenantId);
|
var queryMismatchedUser = new GetBookRevisionsQuery(bookId, "different-user", tenantId);
|
||||||
var resultUser = await handler.Handle(queryMismatchedUser, CancellationToken.None);
|
var actionUser = () => handler.Handle(queryMismatchedUser, CancellationToken.None);
|
||||||
resultUser.IsSuccess.Should().BeFalse();
|
await actionUser.Should().ThrowAsync<BookNotFoundException>();
|
||||||
resultUser.Errors.Should().Contain(e => e.Message.Contains("was not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user