mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-20 05:30:30 +00:00
✨ Add removeFromMine functionality to Meetups API for removing meetups from a user's "My Meetups" list
- 🔒 Introduce `removeFromMine` policy for authenticated users to remove meetups. - ✏️ Add `removeFromMine` method in `MeetupController` with idempotent handling. - ✨ Add `removeMember` utility in `Meetup` model for managing pivot relationships. - 🧪 Add feature tests for `removeFromMine`, covering idempotency, permissions, and unknown slugs. - 🌐 Register `removeFromMine` route in API and link it to `MeetupController`.
This commit is contained in:
@@ -144,6 +144,24 @@ class MeetupController extends Controller
|
|||||||
: \Symfony\Component\HttpFoundation\Response::HTTP_OK);
|
: \Symfony\Component\HttpFoundation\Response::HTTP_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meetup aus „Meine Meetups" entfernen
|
||||||
|
*
|
||||||
|
* Entfernt ein Meetup aus der „Meine Meetups"-Liste des authentifizierten Nutzers
|
||||||
|
* (löst die meetup_user-Pivot-Mitgliedschaft). Die Stammdaten des Meetups bleiben
|
||||||
|
* erhalten — Gegenstück zu addToMine(). Idempotent: war das Meetup nicht (mehr)
|
||||||
|
* zugeordnet, bleibt die Antwort 200 OK.
|
||||||
|
*/
|
||||||
|
#[Response(status: 401, description: 'Nicht authentifiziert.')]
|
||||||
|
public function removeFromMine(Request $request, Meetup $meetup): MeetupResource
|
||||||
|
{
|
||||||
|
Gate::authorize('removeFromMine', $meetup);
|
||||||
|
|
||||||
|
$meetup->removeMember($request->user());
|
||||||
|
|
||||||
|
return MeetupResource::make($meetup);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eigenes Meetup anzeigen
|
* Eigenes Meetup anzeigen
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -202,6 +202,17 @@ class Meetup extends Model implements HasMedia
|
|||||||
return ! $wasMember;
|
return ! $wasMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Den Nutzer wieder aus „Meine Meetups" entfernen (löst die meetup_user-Pivot).
|
||||||
|
* Idempotent: war der Nutzer kein Mitglied, passiert nichts. Gibt true zurück,
|
||||||
|
* wenn tatsächlich eine Zuordnung gelöst wurde (false = war kein Mitglied).
|
||||||
|
* Gegenstück zu {@see addMember()}; die Stammdaten bleiben unberührt.
|
||||||
|
*/
|
||||||
|
public function removeMember(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->users()->detach($user->getKey()) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RateLimiter-Key für Meetup-Stammdaten-Updates über das Portal-Frontend.
|
* RateLimiter-Key für Meetup-Stammdaten-Updates über das Portal-Frontend.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -47,6 +47,17 @@ class MeetupPolicy
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Meetup wieder aus „Meine Meetups" entfernen (löst die meetup_user-Pivot
|
||||||
|
* des Nutzers). Spiegelt die offene Semantik von addToMine(): jeder
|
||||||
|
* authentifizierte Nutzer darf seine eigene Zuordnung lösen. Die Stammdaten
|
||||||
|
* bleiben dem Ersteller vorbehalten (siehe update()).
|
||||||
|
*/
|
||||||
|
public function removeFromMine(User $user, Meetup $meetup): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function update(User $user, Meetup $meetup): bool
|
public function update(User $user, Meetup $meetup): bool
|
||||||
{
|
{
|
||||||
return $this->owns($user, $meetup);
|
return $this->owns($user, $meetup);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Route::middleware('auth:sanctum')
|
|||||||
Route::post('meetup/{meetup}/logo', [MeetupController::class, 'uploadLogo'])->name('meetup.logo');
|
Route::post('meetup/{meetup}/logo', [MeetupController::class, 'uploadLogo'])->name('meetup.logo');
|
||||||
Route::get('my-meetups', [MeetupController::class, 'mine'])->name('meetup.mine');
|
Route::get('my-meetups', [MeetupController::class, 'mine'])->name('meetup.mine');
|
||||||
Route::post('my-meetups/{meetup:slug}', [MeetupController::class, 'addToMine'])->name('meetup.mine.add');
|
Route::post('my-meetups/{meetup:slug}', [MeetupController::class, 'addToMine'])->name('meetup.mine.add');
|
||||||
|
Route::delete('my-meetups/{meetup:slug}', [MeetupController::class, 'removeFromMine'])->name('meetup.mine.remove');
|
||||||
Route::get('my-meetups/{meetup}', [MeetupController::class, 'mineShow'])->name('meetup.mine.show');
|
Route::get('my-meetups/{meetup}', [MeetupController::class, 'mineShow'])->name('meetup.mine.show');
|
||||||
|
|
||||||
Route::post('meetup-events', [MeetupEventController::class, 'store'])->name('meetup-events.store');
|
Route::post('meetup-events', [MeetupEventController::class, 'store'])->name('meetup-events.store');
|
||||||
|
|||||||
@@ -153,3 +153,61 @@ it('returns 404 for an unknown meetup slug', function () {
|
|||||||
|
|
||||||
$this->postJson('/api/my-meetups/does-not-exist-9999')->assertNotFound();
|
$this->postJson('/api/my-meetups/does-not-exist-9999')->assertNotFound();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects a guest removing a meetup from mine', function () {
|
||||||
|
$meetup = Meetup::factory()->create();
|
||||||
|
|
||||||
|
$this->deleteJson('/api/my-meetups/'.$meetup->slug)->assertUnauthorized();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets an authenticated user remove a meetup from mine by slug', function () {
|
||||||
|
Sanctum::actingAs($user = User::factory()->create());
|
||||||
|
$meetup = Meetup::factory()->create();
|
||||||
|
$user->meetups()->attach($meetup, ['is_leader' => false]);
|
||||||
|
|
||||||
|
$this->deleteJson('/api/my-meetups/'.$meetup->slug)
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.id', $meetup->id);
|
||||||
|
|
||||||
|
// Nur die Pivot-Zuordnung wird gelöst — die Stammdaten bleiben erhalten.
|
||||||
|
$this->assertDatabaseMissing('meetup_user', [
|
||||||
|
'meetup_id' => $meetup->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('meetups', ['id' => $meetup->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent and returns 200 when the meetup is not mine', function () {
|
||||||
|
Sanctum::actingAs(User::factory()->create());
|
||||||
|
$meetup = Meetup::factory()->create();
|
||||||
|
|
||||||
|
$this->deleteJson('/api/my-meetups/'.$meetup->slug)
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.id', $meetup->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only removes the acting users membership, not other members', function () {
|
||||||
|
$other = User::factory()->create();
|
||||||
|
$meetup = Meetup::factory()->create();
|
||||||
|
$meetup->users()->attach($other, ['is_leader' => false]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user = User::factory()->create());
|
||||||
|
$meetup->users()->attach($user, ['is_leader' => false]);
|
||||||
|
|
||||||
|
$this->deleteJson('/api/my-meetups/'.$meetup->slug)->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('meetup_user', [
|
||||||
|
'meetup_id' => $meetup->id,
|
||||||
|
'user_id' => $other->id,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('meetup_user', [
|
||||||
|
'meetup_id' => $meetup->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when removing an unknown meetup slug', function () {
|
||||||
|
Sanctum::actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$this->deleteJson('/api/my-meetups/does-not-exist-9999')->assertNotFound();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user