mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-01-24 12:03:17 +00:00
🗓️ Add MeetupEventFactory, implement rate limiting for calendar downloads, and enhance test coverage
- **Added:** `MeetupEventFactory` for generating test data. - **Implemented:** Rate limiting (`throttle:calendar`) for `stream-calendar` routes to prevent abuse. - **Enhanced:** `DownloadMeetupCalendar` controller with validation and cleaner query structure. - **Added:** Feature tests for calendar downloading, invalid input handling, and rate limiting.
This commit is contained in:
@@ -17,11 +17,15 @@ class DownloadMeetupCalendar extends Controller
|
|||||||
public function __invoke(Request $request): Response
|
public function __invoke(Request $request): Response
|
||||||
{
|
{
|
||||||
if ($request->has('meetup')) {
|
if ($request->has('meetup')) {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'meetup' => ['required', 'integer'],
|
||||||
|
]);
|
||||||
|
|
||||||
$meetup = Meetup::query()
|
$meetup = Meetup::query()
|
||||||
->with([
|
->with([
|
||||||
'meetupEvents.meetup',
|
'meetupEvents.meetup',
|
||||||
])
|
])
|
||||||
->findOrFail($request->input('meetup'));
|
->findOrFail($validated['meetup']);
|
||||||
$events = $meetup->meetupEvents()->where('start', '>=', now())->get();
|
$events = $meetup->meetupEvents()->where('start', '>=', now())->get();
|
||||||
$image = $meetup->getFirstMediaUrl('logo');
|
$image = $meetup->getFirstMediaUrl('logo');
|
||||||
} elseif ($request->has('my')) {
|
} elseif ($request->has('my')) {
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Support\Carbon;
|
use App\Support\Carbon;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Events\DiagnosingHealth;
|
use Illuminate\Foundation\Events\DiagnosingHealth;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Date;
|
use Illuminate\Support\Facades\Date;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||||
@@ -31,6 +34,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
$this->configureRateLimiting();
|
||||||
|
|
||||||
Livewire::setUpdateRoute(function ($handle) {
|
Livewire::setUpdateRoute(function ($handle) {
|
||||||
return Route::post('/livewire/update', $handle)
|
return Route::post('/livewire/update', $handle)
|
||||||
->middleware(['web', Sample::rate(0)]);
|
->middleware(['web', Sample::rate(0)]);
|
||||||
@@ -46,4 +51,14 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
Model::preventLazyLoading(app()->environment('local'));
|
Model::preventLazyLoading(app()->environment('local'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the rate limiters for the application.
|
||||||
|
*/
|
||||||
|
protected function configureRateLimiting(): void
|
||||||
|
{
|
||||||
|
RateLimiter::for('calendar', function (Request $request) {
|
||||||
|
return Limit::perMinute(60)->by($request->ip());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
database/factories/MeetupEventFactory.php
Normal file
30
database/factories/MeetupEventFactory.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MeetupEvent>
|
||||||
|
*/
|
||||||
|
class MeetupEventFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'meetup_id' => \App\Models\Meetup::factory(),
|
||||||
|
'start' => now()->addWeek(),
|
||||||
|
'location' => fake()->address(),
|
||||||
|
'description' => fake()->paragraph(),
|
||||||
|
'link' => fake()->url(),
|
||||||
|
'attendees' => [],
|
||||||
|
'might_attendees' => [],
|
||||||
|
'created_by' => \App\Models\User::factory(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,8 @@ Route::livewire('/welcome', 'welcome')->name('welcome');
|
|||||||
|
|
||||||
// Stream calendar route to download meetup calendar as ICS file
|
// Stream calendar route to download meetup calendar as ICS file
|
||||||
Route::get('stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::class)
|
Route::get('stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::class)
|
||||||
->name('ics');
|
->name('ics')
|
||||||
|
->middleware('throttle:calendar');
|
||||||
|
|
||||||
// Dashboard redirect route for authenticated users, redirects to German dashboard
|
// Dashboard redirect route for authenticated users, redirects to German dashboard
|
||||||
Route::middleware(['auth'])
|
Route::middleware(['auth'])
|
||||||
@@ -78,7 +79,8 @@ Route::middleware([])
|
|||||||
/* OLD URLS - redirects for legacy URLs */
|
/* OLD URLS - redirects for legacy URLs */
|
||||||
// Redirect old meetup calendar route to new one
|
// Redirect old meetup calendar route to new one
|
||||||
Route::get('meetup/stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::class)
|
Route::get('meetup/stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::class)
|
||||||
->name('ics');
|
->name('ics')
|
||||||
|
->middleware('throttle:calendar');
|
||||||
// Redirect old meetup overview URL to new meetups page
|
// Redirect old meetup overview URL to new meetups page
|
||||||
Route::get('/meetup/overview', function ($country) {
|
Route::get('/meetup/overview', function ($country) {
|
||||||
return redirect("/{$country}/meetups");
|
return redirect("/{$country}/meetups");
|
||||||
|
|||||||
65
tests/Feature/DownloadMeetupCalendarTest.php
Normal file
65
tests/Feature/DownloadMeetupCalendarTest.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Country;
|
||||||
|
use App\Models\Meetup;
|
||||||
|
use App\Models\MeetupEvent;
|
||||||
|
use function Pest\Laravel\get;
|
||||||
|
|
||||||
|
it('redirects when meetup parameter contains invalid characters', function () {
|
||||||
|
$response = get('/stream-calendar?meetup=49)');
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects when meetup parameter is not an integer', function () {
|
||||||
|
$response = get('/stream-calendar?meetup=abc');
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when meetup ID does not exist', function () {
|
||||||
|
$response = get('/stream-calendar?meetup=999999');
|
||||||
|
|
||||||
|
$response->assertStatus(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns calendar for valid meetup ID', function () {
|
||||||
|
$country = Country::factory()->create();
|
||||||
|
$city = \App\Models\City::factory()->create([
|
||||||
|
'country_id' => $country->id,
|
||||||
|
]);
|
||||||
|
$meetup = Meetup::factory()->create([
|
||||||
|
'city_id' => $city->id,
|
||||||
|
]);
|
||||||
|
MeetupEvent::factory()->create([
|
||||||
|
'meetup_id' => $meetup->id,
|
||||||
|
'start' => now()->addDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = get("/stream-calendar?meetup={$meetup->id}");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertHeader('Content-Type', 'text/calendar; charset=utf-8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 429 when rate limit is exceeded', function () {
|
||||||
|
$country = Country::factory()->create();
|
||||||
|
$city = \App\Models\City::factory()->create([
|
||||||
|
'country_id' => $country->id,
|
||||||
|
]);
|
||||||
|
$meetup = Meetup::factory()->create([
|
||||||
|
'city_id' => $city->id,
|
||||||
|
]);
|
||||||
|
MeetupEvent::factory()->create([
|
||||||
|
'meetup_id' => $meetup->id,
|
||||||
|
'start' => now()->addDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Make 61 requests to exceed the 60 per minute limit
|
||||||
|
for ($i = 0; $i < 61; $i++) {
|
||||||
|
$response = get("/stream-calendar?meetup={$meetup->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last request should be rate limited
|
||||||
|
$response->assertStatus(429);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user