From b3a688cf2bf0bcbcefc61d8518d57dfc581504d3 Mon Sep 17 00:00:00 2001 From: BT Date: Sat, 2 May 2026 22:27:06 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20**Tests=20&=20Nullable=20Fixes:**?= =?UTF-8?q?=20Added=20tests=20to=20ensure=20no=20crashes=20when=20nullable?= =?UTF-8?q?=20Livewire=20properties=20are=20explicitly=20set=20to=20`null`?= =?UTF-8?q?.=20Updated=20several=20Livewire=20components=20to=20handle=20n?= =?UTF-8?q?ullable=20properties=20gracefully.=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/livewire/cities/create.blade.php | 4 +-- .../views/livewire/cities/edit.blade.php | 4 +-- .../courses/create-edit-events.blade.php | 8 ++--- .../meetups/create-edit-events.blade.php | 6 ++-- .../views/livewire/meetups/create.blade.php | 4 +-- .../views/livewire/meetups/edit.blade.php | 4 +-- routes/web.php | 27 ++++++++++------- tests/Feature/Cities/CityCrudTest.php | 24 +++++++++++++-- tests/Feature/Livewire/CourseMountTest.php | 30 +++++++++++++++++++ tests/Feature/Livewire/MeetupMountTest.php | 27 +++++++++++++++++ tests/Feature/Meetups/CreateMeetupTest.php | 16 ++++++++++ tests/Feature/Smoke/PublicRoutesTest.php | 30 +++++++++++++++++-- 12 files changed, 154 insertions(+), 30 deletions(-) diff --git a/resources/views/livewire/cities/create.blade.php b/resources/views/livewire/cities/create.blade.php index 01151fe..d9cffa3 100644 --- a/resources/views/livewire/cities/create.blade.php +++ b/resources/views/livewire/cities/create.blade.php @@ -14,8 +14,8 @@ class extends Component { public $country = 'de'; public string $name = ''; public ?int $country_id = null; - public float $latitude = 0; - public float $longitude = 0; + public ?float $latitude = null; + public ?float $longitude = null; public ?int $population = null; public ?string $population_date = null; diff --git a/resources/views/livewire/cities/edit.blade.php b/resources/views/livewire/cities/edit.blade.php index 612df45..4319296 100644 --- a/resources/views/livewire/cities/edit.blade.php +++ b/resources/views/livewire/cities/edit.blade.php @@ -14,8 +14,8 @@ class extends Component { public City $city; public string $name = ''; public ?int $country_id = null; - public float $latitude = 0; - public float $longitude = 0; + public ?float $latitude = null; + public ?float $longitude = null; public ?int $population = null; public ?string $population_date = null; diff --git a/resources/views/livewire/courses/create-edit-events.blade.php b/resources/views/livewire/courses/create-edit-events.blade.php index 6c0b4da..dd2ee34 100644 --- a/resources/views/livewire/courses/create-edit-events.blade.php +++ b/resources/views/livewire/courses/create-edit-events.blade.php @@ -21,10 +21,10 @@ class extends Component { #[Locked] public $country = 'de'; - public string $fromDate = ''; - public string $fromTime = ''; - public string $toDate = ''; - public string $toTime = ''; + public ?string $fromDate = null; + public ?string $fromTime = null; + public ?string $toDate = null; + public ?string $toTime = null; #[Validate('required|exists:venues,id')] public ?int $venue_id = null; diff --git a/resources/views/livewire/meetups/create-edit-events.blade.php b/resources/views/livewire/meetups/create-edit-events.blade.php index 8a14985..318223b 100644 --- a/resources/views/livewire/meetups/create-edit-events.blade.php +++ b/resources/views/livewire/meetups/create-edit-events.blade.php @@ -20,14 +20,14 @@ class extends Component { #[Locked] public $country = 'de'; - public string $startDate = ''; - public string $startTime = ''; + public ?string $startDate = null; + public ?string $startTime = null; // Explicitly track timezone for reactivity public string $userTimezone = ''; public bool $seriesMode = false; - public string $endDate = ''; + public ?string $endDate = null; public ?RecurrenceType $recurrenceType = null; public ?string $recurrenceDayOfWeek = null; diff --git a/resources/views/livewire/meetups/create.blade.php b/resources/views/livewire/meetups/create.blade.php index 55e3f4c..b8bb054 100644 --- a/resources/views/livewire/meetups/create.blade.php +++ b/resources/views/livewire/meetups/create.blade.php @@ -39,8 +39,8 @@ class extends Component { // New City Modal public string $newCityName = ''; public ?int $newCityCountryId = null; - public float $newCityLatitude = 0; - public float $newCityLongitude = 0; + public ?float $newCityLatitude = null; + public ?float $newCityLongitude = null; public function createCity(): void { diff --git a/resources/views/livewire/meetups/edit.blade.php b/resources/views/livewire/meetups/edit.blade.php index a259290..7ea8e30 100644 --- a/resources/views/livewire/meetups/edit.blade.php +++ b/resources/views/livewire/meetups/edit.blade.php @@ -56,8 +56,8 @@ class extends Component { // New City Modal public string $newCityName = ''; public ?int $newCityCountryId = null; - public float $newCityLatitude = 0; - public float $newCityLongitude = 0; + public ?float $newCityLatitude = null; + public ?float $newCityLongitude = null; public function createCity(): void { diff --git a/routes/web.php b/routes/web.php index 2a5c0de..830959b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,10 @@ = 400 && $code <= 599 ? $code : 404); +})->where('code', '[0-9]{3}'); /* * Commented out routes related to book rental download and display @@ -38,16 +45,16 @@ Route::middleware([]) ->name('buecherverleih');*/ // Route for rabbit following helper page - Updated for Livewire v4 -Route::livewire('/kaninchenbau', \App\Livewire\Helper\FollowTheRabbit::class) +Route::livewire('/kaninchenbau', FollowTheRabbit::class) ->name('kaninchenbau'); // Generic image handler route that serves images from storage -Route::get('/img/{path}', \App\Http\Controllers\ImageController::class) +Route::get('/img/{path}', ImageController::class) ->where('path', '.*') ->name('img'); // Public image handler route for serving public images -Route::get('/img-public/{path}', \App\Http\Controllers\ImageController::class) +Route::get('/img-public/{path}', ImageController::class) ->where('path', '.*') ->name('imgPublic'); @@ -55,7 +62,7 @@ Route::get('/img-public/{path}', \App\Http\Controllers\ImageController::class) Route::livewire('/welcome', 'welcome')->name('welcome'); // Stream calendar route to download meetup calendar as ICS file -Route::get('stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::class) +Route::get('stream-calendar', DownloadMeetupCalendar::class) ->name('ics') ->middleware(['throttle:calendar', Sample::never()]); @@ -78,7 +85,7 @@ Route::middleware([]) ->group(function () { /* OLD URLS - redirects for legacy URLs */ // Redirect old meetup calendar route to new one - Route::get('meetup/stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::class) + Route::get('meetup/stream-calendar', DownloadMeetupCalendar::class) ->name('ics-meetup') ->middleware(['throttle:calendar', Sample::never()]); // Redirect old meetup overview URL to new meetups page diff --git a/tests/Feature/Cities/CityCrudTest.php b/tests/Feature/Cities/CityCrudTest.php index 3936f74..c45f245 100644 --- a/tests/Feature/Cities/CityCrudTest.php +++ b/tests/Feature/Cities/CityCrudTest.php @@ -22,12 +22,32 @@ it('creates a City with valid data', function () { expect(City::query()->where('name', 'Berlin')->exists())->toBeTrue(); }); -it('rejects city creation without a name (country_id is preset by mount() from the route prefix)', function () { +it('rejects city creation when required fields are blank (country_id is preset by mount() from the route prefix)', function () { actingAsUser(); Livewire::test('cities.create') ->call('createCity') - ->assertHasErrors(['name' => 'required']); + ->assertHasErrors([ + 'name' => 'required', + 'latitude' => 'required', + 'longitude' => 'required', + ]); +}); + +it('does not crash with PropertyNotFoundException when latitude is set to null', function () { + actingAsUser(); + Livewire::test('cities.create') + ->set('latitude', null) + ->assertStatus(200) + ->assertSet('latitude', null); +}); + +it('does not crash with PropertyNotFoundException when longitude is set to null', function () { + actingAsUser(); + Livewire::test('cities.create') + ->set('longitude', null) + ->assertStatus(200) + ->assertSet('longitude', null); }); it('rejects city creation when country_id is explicitly cleared', function () { diff --git a/tests/Feature/Livewire/CourseMountTest.php b/tests/Feature/Livewire/CourseMountTest.php index eb7cbbc..9935ef5 100644 --- a/tests/Feature/Livewire/CourseMountTest.php +++ b/tests/Feature/Livewire/CourseMountTest.php @@ -53,3 +53,33 @@ it('mounts courses.create-edit-events for existing event', function () { 'event' => $this->event, ])->assertStatus(200); }); + +it('does not crash with PropertyNotFoundException when fromDate is set to null', function () { + actingAsUser(); + Livewire::test('courses.create-edit-events', ['course' => $this->course]) + ->set('fromDate', null) + ->assertStatus(200) + ->assertSet('fromDate', null); +}); + +it('does not crash when toDate is set to null', function () { + actingAsUser(); + Livewire::test('courses.create-edit-events', ['course' => $this->course]) + ->set('toDate', null) + ->assertStatus(200) + ->assertSet('toDate', null); +}); + +it('does not crash when fromTime is set to null', function () { + actingAsUser(); + Livewire::test('courses.create-edit-events', ['course' => $this->course]) + ->set('fromTime', null) + ->assertStatus(200); +}); + +it('does not crash when toTime is set to null', function () { + actingAsUser(); + Livewire::test('courses.create-edit-events', ['course' => $this->course]) + ->set('toTime', null) + ->assertStatus(200); +}); diff --git a/tests/Feature/Livewire/MeetupMountTest.php b/tests/Feature/Livewire/MeetupMountTest.php index 07dc0cc..e67b690 100644 --- a/tests/Feature/Livewire/MeetupMountTest.php +++ b/tests/Feature/Livewire/MeetupMountTest.php @@ -46,3 +46,30 @@ it('mounts meetups.create-edit-events for existing event', function () { 'event' => $this->event, ])->assertStatus(200); }); + +it('does not crash with PropertyNotFoundException when startDate is set to null in series mode', function () { + actingAsUser(); + Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup]) + ->set('seriesMode', true) + ->set('endDate', '2026-10-27') + ->set('startDate', null) + ->assertStatus(200) + ->assertSet('startDate', null); +}); + +it('does not crash when endDate is set to null in series mode', function () { + actingAsUser(); + Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup]) + ->set('seriesMode', true) + ->set('endDate', null) + ->assertStatus(200) + ->assertSet('endDate', null); +}); + +it('does not crash when startTime is set to null', function () { + actingAsUser(); + Livewire::test('meetups.create-edit-events', ['meetup' => $this->meetup]) + ->set('startTime', null) + ->assertStatus(200) + ->assertSet('startTime', null); +}); diff --git a/tests/Feature/Meetups/CreateMeetupTest.php b/tests/Feature/Meetups/CreateMeetupTest.php index 81fe81e..bdb5396 100644 --- a/tests/Feature/Meetups/CreateMeetupTest.php +++ b/tests/Feature/Meetups/CreateMeetupTest.php @@ -98,3 +98,19 @@ it('creates a city via createCity within the meetup-create flow', function () { expect(City::query()->where('name', 'Hamburg')->exists())->toBeTrue(); }); + +it('does not crash with PropertyNotFoundException when newCityLatitude is set to null', function () { + actingAsUser(); + Livewire::test('meetups.create') + ->set('newCityLatitude', null) + ->assertStatus(200) + ->assertSet('newCityLatitude', null); +}); + +it('does not crash with PropertyNotFoundException when newCityLongitude is set to null', function () { + actingAsUser(); + Livewire::test('meetups.create') + ->set('newCityLongitude', null) + ->assertStatus(200) + ->assertSet('newCityLongitude', null); +}); diff --git a/tests/Feature/Smoke/PublicRoutesTest.php b/tests/Feature/Smoke/PublicRoutesTest.php index fd3f2b4..4bee343 100644 --- a/tests/Feature/Smoke/PublicRoutesTest.php +++ b/tests/Feature/Smoke/PublicRoutesTest.php @@ -55,6 +55,30 @@ it('returns 404 for the application fallback route', function () { $this->get('/this-route-does-not-exist')->assertNotFound(); }); -it('aborts with the requested status code for /error/{code}', function () { - $this->get('/error/418')->assertStatus(418); -}); +it('aborts with the requested status code for /error/{code}', function (int $code) { + $this->get('/error/'.$code)->assertStatus($code); +})->with([ + 'teapot' => 418, + 'forbidden' => 403, + 'not found' => 404, + 'server error' => 500, +]); + +it('returns 404 for /error/{code} when the code is not three digits', function (string $path) { + $this->get($path)->assertNotFound(); +})->with([ + 'non-numeric' => '/error/error.log', + 'letters' => '/error/abc', + 'two digits' => '/error/42', + 'four digits' => '/error/1000', + 'mixed' => '/error/4xx', + 'path traversal' => '/error/..%2Fetc%2Fpasswd', +]); + +it('returns 404 for /error/{code} when code is outside the 4xx-5xx range', function (string $path) { + $this->get($path)->assertNotFound(); +})->with([ + 'success' => '/error/200', + 'redirect' => '/error/301', + 'too high' => '/error/600', +]);