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',