Files
timebank-cc-public/tests/Feature/LivewireLockedPropertiesTest.php

120 lines
4.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Livewire\MainPage\CallCardCarousel;
use App\Http\Livewire\ReactionButton;
use App\Models\User;
use Cog\Laravel\Love\ReactionType\Models\ReactionType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Livewire lets the browser change any public property that is not #[Locked]. On production a bot sent arrays into
* ReactionButton and the call carousel (2026-10-19), and ReactionButton's own permission flags could be switched:
* requiresInteraction off gave a star without a transaction, interactionMethod named any method of the visitor's
* profile. Those properties are locked now, and the actions re-check everything on the server.
*/
class LivewireLockedPropertiesTest extends TestCase
{
use DatabaseTransactions;
private User $me;
private User $other;
protected function setUp(): void
{
parent::setUp();
// Laravel Love caches reaction types in a static array that survives the rollback of an earlier test
(new \ReflectionProperty(ReactionType::class, 'nameCache'))->setValue(null, []);
foreach (['Like' => 1, 'Star' => 1] as $name => $mass) {
ReactionType::query()->firstOrCreate(['name' => $name], ['mass' => $mass]);
}
config([config('app.timebank_config') . '.reactions.star' => ['enabled' => true, 'only_with_interaction' => true, 'interaction' => 'hasTransactionsWith']]);
config([config('app.timebank_config') . '.reactions.like' => ['enabled' => true, 'only_with_interaction' => false, 'interaction' => 'hasTransactionsWith']]);
[$this->me, $this->other] = User::factory()->count(2)->create();
$this->me->refresh();
$this->other->refresh();
$this->actAs($this->me);
}
private function actAs(User $user): void
{
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id, 'active_guard' => 'web']);
}
private function button(string $type)
{
return Livewire::test(ReactionButton::class, ['typeName' => $type, 'modelInstance' => $this->other]);
}
private function hasReacted(string $type): bool
{
return $this->me->fresh()->viaLoveReacter()->hasReactedTo($this->other->fresh(), $type);
}
public function test_the_permission_flags_cannot_be_changed_from_the_browser(): void
{
foreach (['requiresInteraction' => false, 'isEnabled' => true, 'canReact' => true, 'interactionMethod' => 'delete', 'typeName' => ['x'], 'reactedByReactor' => false] as $property => $value) {
try {
$this->button('star')->set($property, $value);
$this->fail("{$property} could be set from the browser");
} catch (CannotUpdateLockedPropertyException) {
$this->addToAssertionCount(1);
}
}
}
public function test_the_confirmation_dialog_stays_settable(): void
{
$this->button('like')->set('showConfirmModal', true)->assertSet('showConfirmModal', true);
}
public function test_a_star_needs_a_transaction_also_when_the_action_is_called_directly(): void
{
$this->button('star')->assertSet('canReact', false)->call('confirmReaction');
$this->assertFalse($this->hasReacted('star'));
}
public function test_a_like_works_once(): void
{
$component = $this->button('like')->call('confirmReaction')->call('confirmReaction');
$this->assertTrue($this->hasReacted('like'));
$component->assertSet('count', 1);
$this->assertSame(1, DB::table('love_reactions')->where('reactant_id', $this->other->fresh()->love_reactant_id)->count());
}
public function test_an_unknown_reaction_type_becomes_a_like(): void
{
Livewire::test(ReactionButton::class, ['typeName' => 'no-such-type', 'modelInstance' => $this->other])
->assertSet('typeName', 'like')
->assertStatus(200);
}
public function test_a_profile_switch_in_another_tab_is_refused(): void
{
$component = $this->button('like');
$third = User::factory()->create();
session(['activeProfileId' => $third->id]);
$component->call('confirmReaction');
$this->assertFalse($this->hasReacted('like'));
}
public function test_the_call_carousel_cannot_be_changed_from_the_browser(): void
{
$this->expectException(CannotUpdateLockedPropertyException::class);
Livewire::test(CallCardCarousel::class, ['related' => false])->assertStatus(200)->set('calls', [1, 2]);
}
}