diff --git a/extend.php b/extend.php index cb0e77d..19e2451 100644 --- a/extend.php +++ b/extend.php @@ -11,12 +11,7 @@ namespace FoF\BestAnswer; -use Flarum\Api\Controller\ListDiscussionsController; -use Flarum\Api\Controller\ListPostsController; -use Flarum\Api\Controller\ListUsersController; -use Flarum\Api\Controller\ShowDiscussionController; -use Flarum\Api\Controller\ShowPostController; -use Flarum\Api\Controller\UpdateDiscussionController; +use Flarum\Api\Controller; use Flarum\Api\Serializer; use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving as DiscussionSaving; @@ -29,7 +24,6 @@ use Flarum\Tags\Api\Serializer\TagSerializer; use Flarum\Tags\Tag; use Flarum\User\User; -use FoF\BestAnswer\Events\BestAnswerSet; return [ (new Extend\Frontend('forum')) @@ -42,6 +36,9 @@ new Extend\Locales(__DIR__.'/resources/locale'), + (new Extend\ServiceProvider()) + ->register(Providers\BestAnswerServiceProvider::class), + (new Extend\Model(Discussion::class)) ->belongsTo('bestAnswerPost', Post::class, 'best_answer_post_id') ->belongsTo('bestAnswerUser', User::class, 'best_answer_user_id') @@ -62,7 +59,7 @@ (new Extend\Event()) ->listen(DiscussionSaving::class, Listeners\SaveBestAnswerToDatabase::class) - ->listen(BestAnswerSet::class, Listeners\QueueNotificationJobs::class) + ->listen(Events\BestAnswerSet::class, Listeners\QueueNotificationJobs::class) ->subscribe(Listeners\RecalculateBestAnswerCounts::class) ->listen(SettingsSaving::class, Listeners\SaveTagSettings::class), @@ -72,17 +69,17 @@ ->type(Notification\BestAnswerSetInDiscussionBlueprint::class, Serializer\BasicDiscussionSerializer::class, []), (new Extend\ApiSerializer(Serializer\DiscussionSerializer::class)) - ->attributes(DiscussionAttributes::class), + ->attributes(Api\DiscussionAttributes::class), (new Extend\ApiSerializer(Serializer\BasicDiscussionSerializer::class)) ->hasOne('bestAnswerPost', Serializer\BasicPostSerializer::class) ->hasOne('bestAnswerUser', Serializer\BasicUserSerializer::class) - ->attributes(BasicDiscussionAttributes::class), + ->attributes(Api\BasicDiscussionAttributes::class), (new Extend\ApiSerializer(Serializer\UserSerializer::class)) - ->attributes(UserBestAnswerCount::class), + ->attributes(Api\UserBestAnswerCount::class), - (new Extend\ApiController(ListUsersController::class)) + (new Extend\ApiController(Controller\ListUsersController::class)) ->addSortField('bestAnswerCount'), (new Extend\Settings()) @@ -97,23 +94,23 @@ ->serializeToForum('fof-best-answer.show_max_lines', 'fof-best-answer.show_max_lines', 'intVal'), (new Extend\ApiSerializer(Serializer\ForumSerializer::class)) - ->attributes(ForumAttributes::class), + ->attributes(Api\ForumAttributes::class), - (new Extend\ApiController(ShowDiscussionController::class)) + (new Extend\ApiController(Controller\ShowDiscussionController::class)) ->addInclude(['bestAnswerPost', 'bestAnswerUser', 'bestAnswerPost.user']) ->load(['bestAnswerPost', 'bestAnswerPost.user']), - (new Extend\ApiController(ListDiscussionsController::class)) + (new Extend\ApiController(Controller\ListDiscussionsController::class)) ->addOptionalInclude(['bestAnswerPost', 'bestAnswerUser', 'bestAnswerPost.discussion', 'bestAnswerPost.user']), - (new Extend\ApiController(UpdateDiscussionController::class)) + (new Extend\ApiController(Controller\UpdateDiscussionController::class)) ->addOptionalInclude('tags'), - (new Extend\ApiController(ListPostsController::class)) + (new Extend\ApiController(Controller\ListPostsController::class)) ->addInclude(['discussion', 'discussion.bestAnswerPost', 'discussion.bestAnswerUser', 'discussion.bestAnswerPost.user']) ->load(['discussion', 'discussion.bestAnswerUser', 'discussion.bestAnswerPost', 'discussion.bestAnswerPost.user']), - (new Extend\ApiController(ShowPostController::class)) + (new Extend\ApiController(Controller\ShowPostController::class)) ->addInclude(['discussion', 'discussion.bestAnswerPost', 'discussion.bestAnswerUser', 'discussion.bestAnswerPost.user']) ->load(['discussion', 'discussion.bestAnswerUser', 'discussion.bestAnswerPost', 'discussion.bestAnswerPost.user']), @@ -132,10 +129,5 @@ ->addFilter(Search\BestAnswerPostFilter::class), (new Extend\ApiSerializer(TagSerializer::class)) - ->attributes(function (TagSerializer $serializer, Tag $tag, array $attributes) { - $attributes['isQnA'] = (bool) $tag->is_qna; - $attributes['reminders'] = (bool) $tag->qna_reminders; - - return $attributes; - }), + ->attributes(Api\AddTagAttributes::class), ]; diff --git a/js/src/forum/extend.ts b/js/src/forum/extend.ts index 27b65a2..38c5cd3 100644 --- a/js/src/forum/extend.ts +++ b/js/src/forum/extend.ts @@ -11,7 +11,7 @@ export default [ new Extend.Model(Discussion) // .hasOne('bestAnswerPost') .hasOne('bestAnswerUser') - .attribute('hasBestAnswer') + .attribute('hasBestAnswer') .attribute('canSelectBestAnswer') .attribute('bestAnswerSetAt', Model.transformDate), diff --git a/src/Api/AddTagAttributes.php b/src/Api/AddTagAttributes.php new file mode 100644 index 0000000..7e94e11 --- /dev/null +++ b/src/Api/AddTagAttributes.php @@ -0,0 +1,26 @@ +is_qna; + $attributes['reminders'] = (bool) $tag->qna_reminders; + + return $attributes; + } +} diff --git a/src/BasicDiscussionAttributes.php b/src/Api/BasicDiscussionAttributes.php similarity index 96% rename from src/BasicDiscussionAttributes.php rename to src/Api/BasicDiscussionAttributes.php index 2419d31..882c71b 100644 --- a/src/BasicDiscussionAttributes.php +++ b/src/Api/BasicDiscussionAttributes.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace FoF\BestAnswer; +namespace FoF\BestAnswer\Api; use Flarum\Api\Serializer\BasicDiscussionSerializer; use Flarum\Discussion\Discussion; diff --git a/src/DiscussionAttributes.php b/src/Api/DiscussionAttributes.php similarity index 91% rename from src/DiscussionAttributes.php rename to src/Api/DiscussionAttributes.php index 05400e6..889d0e1 100644 --- a/src/DiscussionAttributes.php +++ b/src/Api/DiscussionAttributes.php @@ -9,10 +9,11 @@ * file that was distributed with this source code. */ -namespace FoF\BestAnswer; +namespace FoF\BestAnswer\Api; use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Discussion\Discussion; +use FoF\BestAnswer\Repository\BestAnswerRepository; class DiscussionAttributes { diff --git a/src/ForumAttributes.php b/src/Api/ForumAttributes.php similarity index 98% rename from src/ForumAttributes.php rename to src/Api/ForumAttributes.php index 67cf1a8..6c038a1 100644 --- a/src/ForumAttributes.php +++ b/src/Api/ForumAttributes.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace FoF\BestAnswer; +namespace FoF\BestAnswer\Api; use Flarum\Api\Serializer\ForumSerializer; use Flarum\Settings\SettingsRepositoryInterface; diff --git a/src/UserBestAnswerCount.php b/src/Api/UserBestAnswerCount.php similarity index 90% rename from src/UserBestAnswerCount.php rename to src/Api/UserBestAnswerCount.php index 3b94001..799c5a4 100644 --- a/src/UserBestAnswerCount.php +++ b/src/Api/UserBestAnswerCount.php @@ -9,10 +9,11 @@ * file that was distributed with this source code. */ -namespace FoF\BestAnswer; +namespace FoF\BestAnswer\Api; use Flarum\Api\Serializer\UserSerializer; use Flarum\User\User; +use FoF\BestAnswer\Repository\BestAnswerRepository; class UserBestAnswerCount { diff --git a/src/BestAnswerRepository.php b/src/BestAnswerRepository.php deleted file mode 100644 index 00d4bab..0000000 --- a/src/BestAnswerRepository.php +++ /dev/null @@ -1,103 +0,0 @@ -settings = $settings; - } - - public function canSelectBestAnswer(User $user, Discussion $discussion): bool - { - // Prevent best answers being set in a private discussion (ie byobu, etc) - if ($discussion->is_private) { - return false; - } - - return self::tagEnabledForBestAnswer($discussion) && ($user->id === $discussion->user_id - ? $user->can('selectBestAnswerOwnDiscussion', $discussion) - : $user->can('selectBestAnswerNotOwnDiscussion', $discussion)); - } - - public function canSelectPostAsBestAnswer(User $user, Post $post): bool - { - if (!self::canSelectBestAnswer($user, $post->discussion)) { - return false; - } - - if ($user->id === $post->user_id) { - return (bool) $this->settings->get('fof-best-answer.allow_select_own_post'); - } - - return true; - } - - public function canRemoveBestAnswer(User $user, Discussion $discussion): bool - { - return self::canSelectBestAnswer($user, $discussion); - } - - public function tagEnabledForBestAnswer(Discussion $discussion): bool - { - $enabled = false; - - /** @phpstan-ignore-next-line */ - $discussionTags = $discussion->tags; - foreach ($discussionTags as $discussionTag) { - if ((bool) $discussionTag->is_qna) { - $enabled = true; - break; - } - } - - return $enabled; - } - - /** - * Calculate the number of best answers for a user. - * This is used when `best_answer_count` is `null` on the user, usually because either the user - * has not already been awarded any best answers, or the extension was updated to include this feature - * and the user has not yet been serialized. - * - * @param User $user - * - * @return int - */ - public function calculateBestAnswersForUser(User $user): int - { - $count = Discussion::whereNotNull('best_answer_post_id') - ->leftJoin('posts', 'posts.id', '=', 'discussions.best_answer_post_id') - ->where('posts.user_id', $user->id) - ->count(); - - // Use a standalone query and not attribute update+save because otherwise data added by extensions - // with Extend\ApiController::prepareDataForSerialization() ends up being added to the SQL UPDATE clause, - // and breaks Flarum since those are often not real columns - $user->newQuery() - ->where('id', $user->id) - ->update(['best_answer_count' => $count]); - - return $count; - } -} diff --git a/src/Console/UpdateBestAnswerCounts.php b/src/Console/UpdateBestAnswerCounts.php index ff2ef56..1d7b470 100644 --- a/src/Console/UpdateBestAnswerCounts.php +++ b/src/Console/UpdateBestAnswerCounts.php @@ -12,7 +12,7 @@ namespace FoF\BestAnswer\Console; use Flarum\User\User; -use FoF\BestAnswer\BestAnswerRepository; +use FoF\BestAnswer\Repository\BestAnswerRepository; use Illuminate\Console\Command; class UpdateBestAnswerCounts extends Command diff --git a/src/Listeners/SaveBestAnswerToDatabase.php b/src/Listeners/SaveBestAnswerToDatabase.php index 4c43d3e..087ad8f 100644 --- a/src/Listeners/SaveBestAnswerToDatabase.php +++ b/src/Listeners/SaveBestAnswerToDatabase.php @@ -11,24 +11,11 @@ namespace FoF\BestAnswer\Listeners; -use Carbon\Carbon; -use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving; -use Flarum\Foundation\ValidationException; -use Flarum\Notification\Notification; use Flarum\Notification\NotificationSyncer; -use Flarum\Post\Post; -use Flarum\Settings\SettingsRepositoryInterface; -use Flarum\Tags\Tag; -use Flarum\User\Exception\PermissionDeniedException; -use Flarum\User\User; -use FoF\BestAnswer\BestAnswerRepository; -use FoF\BestAnswer\Events\BestAnswerSet; -use FoF\BestAnswer\Events\BestAnswerUnset; use FoF\BestAnswer\Notification\SelectBestAnswerBlueprint; -use Illuminate\Events\Dispatcher; +use FoF\BestAnswer\Repository\BestAnswerRepository; use Illuminate\Support\Arr; -use Symfony\Contracts\Translation\TranslatorInterface; class SaveBestAnswerToDatabase { @@ -39,33 +26,15 @@ class SaveBestAnswerToDatabase */ private $notifications; - /** - * @var Dispatcher - */ - private $bus; - - /** - * @var TranslatorInterface - */ - private $translator; - - /** - * @var SettingsRepositoryInterface - */ - private $settings; - /** * @var BestAnswerRepository */ protected $bestAnswer; - public function __construct(NotificationSyncer $notifications, Dispatcher $bus, TranslatorInterface $translator, BestAnswerRepository $bestAnswer, SettingsRepositoryInterface $settings) + public function __construct(NotificationSyncer $notifications, BestAnswerRepository $bestAnswer) { $this->notifications = $notifications; - $this->bus = $bus; - $this->translator = $translator; $this->bestAnswer = $bestAnswer; - $this->settings = $settings; } public function handle(Saving $event) @@ -86,94 +55,8 @@ public function handle(Saving $event) // If 'id' = 0, then we are removing a best answer. $function = $id === 0 ? 'removeBestAnswer' : 'setBestAnswer'; - $this->$function($discussion, $actor, $id); + $this->bestAnswer->$function($discussion, $actor, $id); $this->notifications->delete(new SelectBestAnswerBlueprint($discussion)); } - - protected function removeBestAnswer(Discussion $discussion, User $actor): void - { - if (!$this->bestAnswer->canRemoveBestAnswer($actor, $discussion)) { - throw new PermissionDeniedException(); - } - - /** @var Post|null $post */ - $post = $discussion->bestAnswerPost; - - if (!$post) { - return; - } - - $discussion->best_answer_post_id = null; - $discussion->best_answer_user_id = null; - $discussion->best_answer_set_at = null; - $discussion->unsetRelation('bestAnswerPost'); - $discussion->unsetRelation('bestAnswerUser'); - - $this->changeTags($discussion, 'detach'); - - $discussion->afterSave(function ($discussion) use ($actor, $post) { - $this->bus->dispatch(new BestAnswerUnset($discussion, $post, $actor)); - }); - } - - protected function setBestAnswer(Discussion $discussion, User $actor, int $id): void - { - /** @var Post|null $post */ - $post = $discussion->posts()->find($id); - - if ($id && !$post) { - throw new ValidationException( - [ - 'error' => $this->translator->trans('fof-best-answer.forum.errors.mismatch'), - ] - ); - } - - if ($post && (!$this->bestAnswer->canSelectPostAsBestAnswer($actor, $post) || !$post->isVisibleTo($actor))) { - throw new PermissionDeniedException(); - } - - if ($id) { - $discussion->best_answer_post_id = $post->id; - $discussion->best_answer_user_id = $actor->id; - $discussion->best_answer_set_at = Carbon::now(); - - Notification::where('type', 'selectBestAnswer')->where('subject_id', $discussion->id)->delete(); - - $this->changeTags($discussion, 'attach'); - - $discussion->afterSave(function (Discussion $discussion) use ($actor) { - $post = $discussion->bestAnswerPost; - $this->bus->dispatch(new BestAnswerSet($discussion, $post, $actor)); - }); - } - } - - protected function changeTags(Discussion $discussion, string $method) - { - $tagsToChange = @json_decode($this->settings->get('fof-best-answer.select_best_answer_tags')); - - if (empty($tagsToChange)) { - return; - } - - $validTags = Tag::query()->whereIn('id', $tagsToChange); - - // Query errors if we try to attach tags that are already attached due to the unique constraint - if ($method === 'attach') { - /** @phpstan-ignore-next-line */ - $existingTags = $discussion->tags()->pluck('id'); - $validTags = $validTags->whereNotIn('id', $existingTags); - } - - $validTagsIds = $validTags->pluck('id'); - - if ($validTagsIds->isEmpty()) { - return; - } - - /** @phpstan-ignore-next-line */ - $discussion->tags()->$method($validTagsIds); - } } diff --git a/src/Providers/BestAnswerServiceProvider.php b/src/Providers/BestAnswerServiceProvider.php new file mode 100644 index 0000000..c3f3d74 --- /dev/null +++ b/src/Providers/BestAnswerServiceProvider.php @@ -0,0 +1,23 @@ +container->bind(BestAnswerRepository::class); + } +} diff --git a/src/Repository/BestAnswerRepository.php b/src/Repository/BestAnswerRepository.php new file mode 100644 index 0000000..348fb6c --- /dev/null +++ b/src/Repository/BestAnswerRepository.php @@ -0,0 +1,213 @@ +settings = $settings; + $this->events = $events; + $this->translator = $translator; + } + + public function canSelectBestAnswer(User $user, Discussion $discussion): bool + { + // Prevent best answers being set in a private discussion (ie byobu, etc) + if ($discussion->is_private) { + return false; + } + + return $this->tagEnabledForBestAnswer($discussion) && ($user->id === $discussion->user_id + ? $user->can('selectBestAnswerOwnDiscussion', $discussion) + : $user->can('selectBestAnswerNotOwnDiscussion', $discussion)); + } + + public function canSelectPostAsBestAnswer(User $user, Post $post): bool + { + if (!$this->canSelectBestAnswer($user, $post->discussion)) { + return false; + } + + if ($user->id === $post->user_id) { + return (bool) $this->settings->get('fof-best-answer.allow_select_own_post'); + } + + return true; + } + + public function canRemoveBestAnswer(User $user, Discussion $discussion): bool + { + return $this->canSelectBestAnswer($user, $discussion); + } + + public function tagEnabledForBestAnswer(Discussion $discussion): bool + { + $enabled = false; + + /** @phpstan-ignore-next-line */ + $discussionTags = $discussion->tags; + foreach ($discussionTags as $discussionTag) { + if ((bool) $discussionTag->is_qna) { + $enabled = true; + break; + } + } + + return $enabled; + } + + /** + * Calculate the number of best answers for a user. + * This is used when `best_answer_count` is `null` on the user, usually because either the user + * has not already been awarded any best answers, or the extension was updated to include this feature + * and the user has not yet been serialized. + * + * @param User $user + * + * @return int + */ + public function calculateBestAnswersForUser(User $user): int + { + $count = Discussion::whereNotNull('best_answer_post_id') + ->leftJoin('posts', 'posts.id', '=', 'discussions.best_answer_post_id') + ->where('posts.user_id', $user->id) + ->count(); + + // Use a standalone query and not attribute update+save because otherwise data added by extensions + // with Extend\ApiController::prepareDataForSerialization() ends up being added to the SQL UPDATE clause, + // and breaks Flarum since those are often not real columns + $user->newQuery() + ->where('id', $user->id) + ->update(['best_answer_count' => $count]); + + return $count; + } + + public function removeBestAnswer(Discussion $discussion, User $actor): void + { + if (!$this->canRemoveBestAnswer($actor, $discussion)) { + throw new PermissionDeniedException(); + } + + /** @var Post|null $post */ + $post = $discussion->bestAnswerPost; + + if (!$post) { + return; + } + + $discussion->best_answer_post_id = null; + $discussion->best_answer_user_id = null; + $discussion->best_answer_set_at = null; + $discussion->unsetRelation('bestAnswerPost'); + $discussion->unsetRelation('bestAnswerUser'); + + $this->changeTags($discussion, 'detach'); + + $discussion->afterSave(function ($discussion) use ($actor, $post) { + $this->events->dispatch(new BestAnswerUnset($discussion, $post, $actor)); + }); + } + + public function setBestAnswer(Discussion $discussion, User $actor, int $id): void + { + /** @var Post|null $post */ + $post = $discussion->posts()->find($id); + + if ($id && !$post) { + throw new ValidationException( + [ + 'error' => $this->translator->trans('fof-best-answer.forum.errors.mismatch'), + ] + ); + } + + if ($post && (!$this->canSelectPostAsBestAnswer($actor, $post) || !$post->isVisibleTo($actor))) { + throw new PermissionDeniedException(); + } + + if ($id) { + $discussion->best_answer_post_id = $post->id; + $discussion->best_answer_user_id = $actor->id; + $discussion->best_answer_set_at = Carbon::now(); + + Notification::where('type', 'selectBestAnswer')->where('subject_id', $discussion->id)->delete(); + + $this->changeTags($discussion, 'attach'); + + $discussion->afterSave(function (Discussion $discussion) use ($actor) { + $post = $discussion->bestAnswerPost; + $this->events->dispatch(new BestAnswerSet($discussion, $post, $actor)); + }); + } + } + + public function changeTags(Discussion $discussion, string $method) + { + $tagsToChange = @json_decode($this->settings->get('fof-best-answer.select_best_answer_tags')); + + if (empty($tagsToChange)) { + return; + } + + $validTags = Tag::query()->whereIn('id', $tagsToChange); + + // Query errors if we try to attach tags that are already attached due to the unique constraint + if ($method === 'attach') { + /** @phpstan-ignore-next-line */ + $existingTags = $discussion->tags()->pluck('id'); + $validTags = $validTags->whereNotIn('id', $existingTags); + } + + $validTagsIds = $validTags->pluck('id'); + + if ($validTagsIds->isEmpty()) { + return; + } + + /** @phpstan-ignore-next-line */ + $discussion->tags()->$method($validTagsIds); + } +} diff --git a/tests/unit/SaveBestAnswerToDatabaseTest.php b/tests/unit/SaveBestAnswerToDatabaseTest.php index 75cd08f..7ead57b 100644 --- a/tests/unit/SaveBestAnswerToDatabaseTest.php +++ b/tests/unit/SaveBestAnswerToDatabaseTest.php @@ -14,20 +14,17 @@ use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving; use Flarum\Notification\NotificationSyncer; -use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Testing\unit\TestCase; use Flarum\User\User; -use FoF\BestAnswer\BestAnswerRepository; use FoF\BestAnswer\Listeners\SaveBestAnswerToDatabase; use FoF\BestAnswer\Notification\SelectBestAnswerBlueprint; -use Illuminate\Events\Dispatcher; +use FoF\BestAnswer\Repository\BestAnswerRepository; use Mockery as m; -use Symfony\Contracts\Translation\TranslatorInterface; class SaveBestAnswerToDatabaseTest extends TestCase { /** - * @var m\MockInterface|SaveBestAnswerToDatabase + * @var SaveBestAnswerToDatabase */ protected $sut; // System Under Test @@ -37,25 +34,20 @@ public function tearDown(): void parent::tearDown(); } - // testing the `handle()` method - public function testHandle_WhenKeyIsMissing_ReturnsWithoutAction() { $event = m::mock(Saving::class); $event->data = []; - $this->sut = m::mock(SaveBestAnswerToDatabase::class.'[removeBestAnswer,setBestAnswer]', [ - m::mock(NotificationSyncer::class), - m::mock(Dispatcher::class), - m::mock(TranslatorInterface::class), - m::mock(BestAnswerRepository::class), - m::mock(SettingsRepositoryInterface::class), - ])->shouldAllowMockingProtectedMethods(); + $notifications = m::mock(NotificationSyncer::class); + $repository = m::mock(BestAnswerRepository::class); + + $this->sut = new SaveBestAnswerToDatabase($notifications, $repository); - $this->sut->shouldNotReceive('removeBestAnswer'); - $this->sut->shouldNotReceive('setBestAnswer'); + $result = $this->sut->handle($event); - $this->sut->handle($event); + // Assert that no actions were taken + $this->assertNull($result); } public function testHandle_DiscussionDoesNotExistOrMatches_ReturnsWithoutAction() @@ -68,21 +60,18 @@ public function testHandle_DiscussionDoesNotExistOrMatches_ReturnsWithoutAction( $event->discussion->exists = false; $event->discussion->best_answer_post_id = 1; - $this->sut = m::mock(SaveBestAnswerToDatabase::class.'[removeBestAnswer,setBestAnswer]', [ - m::mock(NotificationSyncer::class), - m::mock(Dispatcher::class), - m::mock(TranslatorInterface::class), - m::mock(BestAnswerRepository::class), - m::mock(SettingsRepositoryInterface::class), - ])->shouldAllowMockingProtectedMethods(); + $notifications = m::mock(NotificationSyncer::class); + $repository = m::mock(BestAnswerRepository::class); - $this->sut->shouldNotReceive('removeBestAnswer'); - $this->sut->shouldNotReceive('setBestAnswer'); + $this->sut = new SaveBestAnswerToDatabase($notifications, $repository); - $this->sut->handle($event); + $result = $this->sut->handle($event); + + // Assert that no actions were taken + $this->assertNull($result); } - public function testHandle_IdIsZero_CallsRemoveBestAnswer() + public function testHandle_IdIsZero_CallsHandleRemoveBestAnswer() { $event = m::mock(Saving::class); $event->data = ['attributes.bestAnswerPostId' => 0]; @@ -95,20 +84,16 @@ public function testHandle_IdIsZero_CallsRemoveBestAnswer() $notifications = m::mock(NotificationSyncer::class); $notifications->shouldReceive('delete')->with(m::type(SelectBestAnswerBlueprint::class))->once(); - $this->sut = m::mock(SaveBestAnswerToDatabase::class.'[removeBestAnswer]', [ - $notifications, - m::mock(Dispatcher::class), - m::mock(TranslatorInterface::class), - m::mock(BestAnswerRepository::class), - m::mock(SettingsRepositoryInterface::class), - ])->shouldAllowMockingProtectedMethods(); + $repository = m::mock(BestAnswerRepository::class); + $repository->shouldReceive('canRemoveBestAnswer')->with($event->actor, $event->discussion)->andReturn(true); + $repository->shouldReceive('removeBestAnswer')->with($event->discussion, $event->actor, 0)->once(); - $this->sut->shouldReceive('removeBestAnswer')->once(); + $this->sut = new SaveBestAnswerToDatabase($notifications, $repository); $this->sut->handle($event); } - public function testHandle_IdIsNotZero_CallsSetBestAnswer() + public function testHandle_IdIsNotZero_CallsHandleSetBestAnswer() { $event = m::mock(Saving::class); $event->data = ['attributes.bestAnswerPostId' => 2]; @@ -121,15 +106,11 @@ public function testHandle_IdIsNotZero_CallsSetBestAnswer() $notifications = m::mock(NotificationSyncer::class); $notifications->shouldReceive('delete')->with(m::type(SelectBestAnswerBlueprint::class))->once(); - $this->sut = m::mock(SaveBestAnswerToDatabase::class.'[setBestAnswer]', [ - $notifications, - m::mock(Dispatcher::class), - m::mock(TranslatorInterface::class), - m::mock(BestAnswerRepository::class), - m::mock(SettingsRepositoryInterface::class), - ])->shouldAllowMockingProtectedMethods(); + $repository = m::mock(BestAnswerRepository::class); + $repository->shouldReceive('canSelectPostAsBestAnswer')->with($event->actor, m::type(Discussion::class))->andReturn(true); + $repository->shouldReceive('setBestAnswer')->with($event->discussion, $event->actor, 2)->once(); - $this->sut->shouldReceive('setBestAnswer')->once(); + $this->sut = new SaveBestAnswerToDatabase($notifications, $repository); $this->sut->handle($event); }