From dc1d679e4b9fc4adbaed737a495307f4dedc9b6e Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode <123783602+HolgerHatGarKeineNode@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:05:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20leader-based=20permissions=20?= =?UTF-8?q?to=20Meetup=20event=20tests=20and=20editable=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🔒 Ensure leader users are required for Meetup event tests to simulate accurate permissions. - ➕ Add `editableBy` scope to `MeetupEvent` model for consistent editable event handling. - 🛠️ Refactor `mine` API endpoint and MCP tool to leverage `editableBy` scope. - 🧪 Update tests to verify leader-based accessibility for Meetup events. --- .../Controllers/Api/MeetupEventController.php | 7 ++++--- .../MeetupEvent/ListMyMeetupEventsTool.php | 4 ++-- app/Models/Meetup.php | 2 +- app/Models/MeetupEvent.php | 13 +++++++++++++ tests/Feature/Api/MeetupEventWriteApiTest.php | 15 +++++++++++++-- tests/Feature/Livewire/MeetupMountTest.php | 15 ++++++++++----- tests/Feature/Mcp/MeetupEventMcpToolTest.php | 2 +- tests/Feature/Mcp/MeetupMembershipMcpTest.php | 18 +++++++++++++++++- 8 files changed, 61 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Api/MeetupEventController.php b/app/Http/Controllers/Api/MeetupEventController.php index 91ca352..30b6802 100644 --- a/app/Http/Controllers/Api/MeetupEventController.php +++ b/app/Http/Controllers/Api/MeetupEventController.php @@ -136,16 +136,17 @@ class MeetupEventController extends Controller } /** - * Eigene Meetup-Events auflisten + * Bearbeitbare Meetup-Events auflisten * - * Liefert alle vom authentifizierten Nutzer erstellten Meetup-Events, nach Startzeit absteigend sortiert. + * Liefert alle Meetup-Events, die der authentifizierte Nutzer bearbeiten darf + * (selbst angelegt ODER Leader des zugehörigen Meetups), nach Startzeit absteigend sortiert. */ public function mine(Request $request): AnonymousResourceCollection { Gate::authorize('viewAny', MeetupEvent::class); $meetupEvents = MeetupEvent::query() - ->where('created_by', $request->user()->id) + ->editableBy($request->user()->id) ->orderByDesc('start') ->get(); diff --git a/app/Mcp/Tools/MeetupEvent/ListMyMeetupEventsTool.php b/app/Mcp/Tools/MeetupEvent/ListMyMeetupEventsTool.php index d275e44..7b1bdb3 100644 --- a/app/Mcp/Tools/MeetupEvent/ListMyMeetupEventsTool.php +++ b/app/Mcp/Tools/MeetupEvent/ListMyMeetupEventsTool.php @@ -12,7 +12,7 @@ use Laravel\Mcp\Server\Tool; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; #[IsReadOnly] -#[Description('Listet alle vom authentifizierten Nutzer erstellten Meetup-Termine, nach Startzeitpunkt absteigend sortiert.')] +#[Description('Listet alle Meetup-Termine, die der authentifizierte Nutzer bearbeiten darf (selbst angelegt oder Leader des Meetups), nach Startzeitpunkt absteigend sortiert.')] class ListMyMeetupEventsTool extends Tool { public function handle(Request $request): Response @@ -24,7 +24,7 @@ class ListMyMeetupEventsTool extends Tool } $meetupEvents = MeetupEvent::query() - ->where('created_by', $user->getAuthIdentifier()) + ->editableBy((int) $user->getAuthIdentifier()) ->orderByDesc('start') ->get(); diff --git a/app/Models/Meetup.php b/app/Models/Meetup.php index 33102a2..8735b54 100644 --- a/app/Models/Meetup.php +++ b/app/Models/Meetup.php @@ -292,7 +292,7 @@ class Meetup extends Model implements HasMedia */ public function scopeLedBy(Builder $query, int $userId): void { - $query->whereHas('users', fn (Builder $user) => $user->whereKey($userId)->wherePivot('is_leader', true)); + $query->whereHas('users', fn (Builder $user) => $user->whereKey($userId)->where('meetup_user.is_leader', true)); } /** diff --git a/app/Models/MeetupEvent.php b/app/Models/MeetupEvent.php index a954d5f..84120ca 100644 --- a/app/Models/MeetupEvent.php +++ b/app/Models/MeetupEvent.php @@ -6,6 +6,7 @@ use App\Enums\RecurrenceType; use App\Enums\RsvpStatus; use App\Observers\MeetupEventObserver; use Illuminate\Database\Eloquent\Attributes\ObservedBy; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -55,6 +56,18 @@ class MeetupEvent extends Model }); } + /** + * Termine, die der Nutzer bearbeiten darf: selbst angelegt ODER Leader des + * zugehörigen Meetups (deckungsgleich mit MeetupEventPolicy::update). + */ + public function scopeEditableBy(Builder $query, int $userId): void + { + $query->where(function (Builder $query) use ($userId) { + $query->where('created_by', $userId) + ->orWhereHas('meetup', fn (Builder $meetup) => $meetup->ledBy($userId)); + }); + } + public function createdBy(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); diff --git a/tests/Feature/Api/MeetupEventWriteApiTest.php b/tests/Feature/Api/MeetupEventWriteApiTest.php index 8086776..3bc50a2 100644 --- a/tests/Feature/Api/MeetupEventWriteApiTest.php +++ b/tests/Feature/Api/MeetupEventWriteApiTest.php @@ -106,17 +106,28 @@ it('forbids updating someone elses', function () { $response->assertForbidden(); }); -it('returns only own in mine index', function () { +it('returns own and led-meetup events in mine index', function () { Sanctum::actingAs($user = User::factory()->create()); + // 2 selbst angelegt MeetupEvent::factory()->count(2)->create(['created_by' => $user->id]); + + // 1 Termin eines Co-Leaders im selben (von $user geführten) Meetup -> sichtbar + $ledMeetup = Meetup::factory()->create(); + $ledMeetup->users()->syncWithoutDetaching([$user->id => ['is_leader' => true]]); + MeetupEvent::factory()->create([ + 'meetup_id' => $ledMeetup->id, + 'created_by' => User::factory()->create()->id, + ]); + + // 1 fremder Termin in einem Meetup ohne Leaderschaft -> NICHT sichtbar MeetupEvent::factory()->create(['created_by' => User::factory()->create()->id]); $response = $this->getJson('/api/my-meetup-events'); $response->assertSuccessful(); - expect($response->json('data'))->toHaveCount(2); + expect($response->json('data'))->toHaveCount(3); }); it('forbids viewing someone elses in mine show', function () { diff --git a/tests/Feature/Livewire/MeetupMountTest.php b/tests/Feature/Livewire/MeetupMountTest.php index a30eb3c..ccc8f15 100644 --- a/tests/Feature/Livewire/MeetupMountTest.php +++ b/tests/Feature/Livewire/MeetupMountTest.php @@ -70,12 +70,14 @@ it('aborts meetups.edit with 403 when the user is neither creator nor super-admi }); it('mounts meetups.create-edit-events for new event', function () { - actingAsUser(); + $leader = actingAsUser(); + $this->meetup->users()->syncWithoutDetaching([$leader->id => ['is_leader' => true]]); Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup])->assertStatus(200); }); it('mounts meetups.create-edit-events for existing event', function () { - actingAsUser(); + $leader = actingAsUser(); + $this->meetup->users()->syncWithoutDetaching([$leader->id => ['is_leader' => true]]); Livewire::test('meetups.create-edit-events', [ 'meetup' => $this->meetup, 'event' => $this->event, @@ -83,7 +85,8 @@ it('mounts meetups.create-edit-events for existing event', function () { }); it('does not crash with PropertyNotFoundException when startDate is set to null in series mode', function () { - actingAsUser(); + $leader = actingAsUser(); + $this->meetup->users()->syncWithoutDetaching([$leader->id => ['is_leader' => true]]); Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup]) ->set('seriesMode', true) ->set('endDate', '2026-10-27') @@ -93,7 +96,8 @@ it('does not crash with PropertyNotFoundException when startDate is set to null }); it('does not crash when endDate is set to null in series mode', function () { - actingAsUser(); + $leader = actingAsUser(); + $this->meetup->users()->syncWithoutDetaching([$leader->id => ['is_leader' => true]]); Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup]) ->set('seriesMode', true) ->set('endDate', null) @@ -102,7 +106,8 @@ it('does not crash when endDate is set to null in series mode', function () { }); it('does not crash when startTime is set to null', function () { - actingAsUser(); + $leader = actingAsUser(); + $this->meetup->users()->syncWithoutDetaching([$leader->id => ['is_leader' => true]]); Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup]) ->set('startTime', null) ->assertStatus(200) diff --git a/tests/Feature/Mcp/MeetupEventMcpToolTest.php b/tests/Feature/Mcp/MeetupEventMcpToolTest.php index 9a96fdf..c83ea46 100644 --- a/tests/Feature/Mcp/MeetupEventMcpToolTest.php +++ b/tests/Feature/Mcp/MeetupEventMcpToolTest.php @@ -11,7 +11,7 @@ use App\Models\User; it('lets an authenticated user create a meetup event and stamps created_by', function () { $user = User::factory()->create(); - $meetup = Meetup::factory()->create(); + $meetup = Meetup::factory()->create(['created_by' => $user->id]); $response = EinundzwanzigServer::actingAs($user)->tool(CreateMeetupEventTool::class, [ 'meetup_id' => $meetup->id, diff --git a/tests/Feature/Mcp/MeetupMembershipMcpTest.php b/tests/Feature/Mcp/MeetupMembershipMcpTest.php index 76766c4..ecda9c7 100644 --- a/tests/Feature/Mcp/MeetupMembershipMcpTest.php +++ b/tests/Feature/Mcp/MeetupMembershipMcpTest.php @@ -53,11 +53,27 @@ it('makes the creator a leader so the meetup shows in my meetups', function () { ]); }); -it('lets a member add an event to a joined meetup', function () { +it('forbids a non-leader member from adding an event but allows a leader', function () { $user = User::factory()->create(); $meetup = Meetup::factory()->create(['name' => 'Einundzwanzig Dortmund']); $meetup->users()->attach($user->id, ['is_leader' => false]); + // Reines Mitglied (is_leader = false) darf keinen Termin anlegen. + EinundzwanzigServer::actingAs($user) + ->tool(CreateMeetupEventTool::class, [ + 'meetup' => 'Einundzwanzig Dortmund', + 'start' => '2026-08-01 18:00:00', + ]) + ->assertHasErrors(); + + $this->assertDatabaseMissing('meetup_events', [ + 'meetup_id' => $meetup->id, + 'created_by' => $user->id, + ]); + + // Als Leader darf derselbe Nutzer den Termin anlegen. + $meetup->users()->syncWithoutDetaching([$user->id => ['is_leader' => true]]); + EinundzwanzigServer::actingAs($user) ->tool(CreateMeetupEventTool::class, [ 'meetup' => 'Einundzwanzig Dortmund',