From 77ceddbb6657248fd6b41ff0e3f03877e561b412 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Wed, 23 Sep 2026 01:10:40 +0100 Subject: [PATCH] Unify CanWrite null-auth stamp between board detail and list (detail now follows the no-enforcement convention) GetBoardDetailAsync(actingUserId) stamped CanWrite=false when no IAuthorizationService was composed, while ListBoardsPaginatedAsync stamped true for the same composition. Null auth (CLI/unauthenticated composition) means no enforcement at all, so both now report the caller can write. Adds two parity tests. --- .../Services/BoardService.cs | 8 +++- .../Services/BoardServiceTests.cs | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/backend/src/Taskdeck.Application/Services/BoardService.cs b/backend/src/Taskdeck.Application/Services/BoardService.cs index 40fdcb8385..9cad6347e5 100644 --- a/backend/src/Taskdeck.Application/Services/BoardService.cs +++ b/backend/src/Taskdeck.Application/Services/BoardService.cs @@ -100,8 +100,12 @@ public async Task> GetBoardDetailAsync(Guid id, Guid acti var detail = await GetBoardDetailAsync(id, cancellationToken); if (!detail.IsSuccess) return detail; - var writable = _authorizationService is null ? null : await _authorizationService.CanWriteBoardAsync(actingUserId, id); - return Result.Success(detail.Value with { CanWrite = writable is { IsSuccess: true, Value: true } }); + var writable = _authorizationService is null + || (await _authorizationService.CanWriteBoardAsync(actingUserId, id)) is { IsSuccess: true, Value: true }; + // No authorization service configured (CLI / unauthenticated composition) means no + // authorization is being enforced at all, so the caller can write — the same + // convention ListBoardsPaginatedAsync and EnsureBoardPermissionAsync follow. + return Result.Success(detail.Value with { CanWrite = writable }); } /// diff --git a/backend/tests/Taskdeck.Application.Tests/Services/BoardServiceTests.cs b/backend/tests/Taskdeck.Application.Tests/Services/BoardServiceTests.cs index 1991e966d8..4e40378daa 100644 --- a/backend/tests/Taskdeck.Application.Tests/Services/BoardServiceTests.cs +++ b/backend/tests/Taskdeck.Application.Tests/Services/BoardServiceTests.cs @@ -588,6 +588,44 @@ public async Task CreateBoardAsync_ShouldStampCanWriteTrue_ForTheCreatingOwner() result.IsSuccess.Should().BeTrue(); result.Value.CanWrite.Should().BeTrue(); } + [Fact] + public async Task GetBoardDetailAsync_ShouldStampCanWriteTrue_WhenNoAuthorizationService() + { + // Null authorization (CLI / unauthenticated composition) means no enforcement, + // so detail must agree with the list stamp: the caller can write. + var board = TestDataBuilder.CreateBoard("Detail Board"); + _boardRepoMock.Setup(r => r.GetByIdWithDetailsAsync(board.Id, default)) + .ReturnsAsync(board); + + var result = await _service.GetBoardDetailAsync(board.Id, Guid.NewGuid()); + + result.IsSuccess.Should().BeTrue(); + result.Value.CanWrite.Should().BeTrue(); + } + + [Fact] + public async Task ListBoardsPaginatedAsync_ShouldStampCanWriteTrue_WhenNoAuthorizationService() + { + // Parity lock for the detail stamp above: with no authorization service every + // visible board is writable. + var actingUserId = Guid.NewGuid(); + var boards = new[] + { + new Board("Board A", "desc", actingUserId), + new Board("Board B", "desc", actingUserId) + }; + var boardIds = boards.Select(b => b.Id).ToList(); + _boardRepoMock.Setup(r => r.SearchIdsAsync(null, false, default)) + .ReturnsAsync(boardIds); + _boardRepoMock.Setup(r => r.GetByIdsAsync(It.IsAny>(), default)) + .ReturnsAsync(boards); + + var result = await _service.ListBoardsPaginatedAsync(actingUserId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Items.Should().HaveCount(2); + result.Value.Items.Should().OnlyContain(b => b.CanWrite); + } #endregion }