fix(board): 반응 처리 락 획득 시점 게시글 삭제 레이스를 404로 처리
스코프 검증 통과 직후·Post 락 획득 시점 사이에 게시글이 삭제되면 lockForUpdate 의 findOrFail 이 ModelNotFoundException 을 던지는데, 컨트롤러가 이 예외를 모르므로 일반 500 으로 새어나가 사용자가 "반응 처리에 실패했습니다" 라는 원인 불명 메시지만 보게 되던 문제를 수정. 이미 존재를 확인한 게시글이 사라진 것과 같은 의미이므로 PostNotFoundException 으로 변환해 404 로 일관 처리.
This commit is contained in:
@@ -107,7 +107,17 @@ class ReactionService
|
||||
// 판단하도록 보장한다. 잠금 없이 existing 을 먼저 읽으면 다른 트랜잭션이
|
||||
// 그 사이 상태를 바꿔도 반영되지 않아, 이미 지워진 반응을 다시 지우거나
|
||||
// 캐시 카운트가 실제 반응 행 수와 어긋나는 결함으로 이어진다.
|
||||
$this->reactionRepository->lockPostForReaction($post->id);
|
||||
//
|
||||
// 스코프 검증 통과 직후·락 획득 시점 사이에 게시글이 삭제되는 레이스에서는
|
||||
// findOrFail() 이 ModelNotFoundException 을 던진다. 컨트롤러는 이 예외를
|
||||
// 모르므로 그대로 두면 일반 500 으로 새어나가 사용자가 원인을 알 수 없다 —
|
||||
// 이미 위에서 존재를 확인한 게시글이 사라진 것과 같은 의미이므로 동일하게
|
||||
// PostNotFoundException 으로 변환한다.
|
||||
try {
|
||||
$this->reactionRepository->lockPostForReaction($post->id);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
throw new PostNotFoundException($post->id);
|
||||
}
|
||||
|
||||
$existing = $this->reactionRepository->findByUserAndTarget(
|
||||
$userId,
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace Modules\Sirsoft\Board\Tests\Unit\Services;
|
||||
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
|
||||
use Modules\Sirsoft\Board\Exceptions\PostNotFoundException;
|
||||
use Modules\Sirsoft\Board\Exceptions\ReactionNotAllowedException;
|
||||
use Modules\Sirsoft\Board\Models\Reaction;
|
||||
use Modules\Sirsoft\Board\Models\ReactionType;
|
||||
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
|
||||
use Modules\Sirsoft\Board\Services\ReactionService;
|
||||
use Modules\Sirsoft\Board\Tests\BoardTestCase;
|
||||
|
||||
@@ -237,4 +239,38 @@ class ReactionServiceTest extends BoardTestCase
|
||||
$this->expectException(PostNotFoundException::class);
|
||||
$this->service->react($reactor->id, $this->board, 999999, $this->likeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프 검증 통과 직후·락 획득 시점 사이에 게시글이 삭제되는 레이스 상황에서도
|
||||
* PostNotFoundException 으로 일관되게 처리된다.
|
||||
*
|
||||
* lockPostForReaction() 은 findOrFail() 을 사용해 락을 거는데, 이 시점에 게시글이
|
||||
* 없으면 Eloquent 의 ModelNotFoundException 이 던져진다. 이를 감싸지 않으면 컨트롤러의
|
||||
* catch 블록 어디에도 걸리지 않아 일반 \Exception 으로 떨어져 500 에러가 되고, 사용자는
|
||||
* "반응 처리에 실패했습니다"만 보게 되어 게시글이 없다는 실제 원인을 알 수 없다.
|
||||
*
|
||||
* @scenario case=post_deleted_between_scope_check_and_lock
|
||||
* @effects post_not_in_board_returns_404
|
||||
*/
|
||||
public function test_react_wraps_model_not_found_during_lock_as_post_not_found(): void
|
||||
{
|
||||
$author = $this->createUser();
|
||||
$reactor = $this->createUser();
|
||||
$postId = $this->createTestPost(['user_id' => $author->id]);
|
||||
|
||||
$repository = $this->mock(ReactionRepositoryInterface::class);
|
||||
$repository->shouldReceive('lockPostForReaction')
|
||||
->once()
|
||||
->with($postId)
|
||||
->andThrow(new ModelNotFoundException());
|
||||
|
||||
$service = new ReactionService(
|
||||
$repository,
|
||||
app(\Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface::class),
|
||||
app(\Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface::class),
|
||||
);
|
||||
|
||||
$this->expectException(PostNotFoundException::class);
|
||||
$service->react($reactor->id, $this->board, $postId, $this->likeId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ axes:
|
||||
- inactive_type_react_blocked
|
||||
- use_reaction_off_react_blocked
|
||||
- post_not_in_board
|
||||
- post_deleted_between_scope_check_and_lock
|
||||
- detail_shows_active_types_even_zero
|
||||
- detail_hidden_when_no_active_types
|
||||
- detail_hidden_when_use_reaction_off
|
||||
|
||||
Reference in New Issue
Block a user