diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..a52b235 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"95f2d618-b0a9-4b52-b725-3a049ae47e93","pid":146659,"procStart":"1263491","acquiredAt":1777828038888} \ No newline at end of file diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/livewire/auth/register.blade.php deleted file mode 100644 index 33601df..0000000 --- a/resources/views/livewire/auth/register.blade.php +++ /dev/null @@ -1,104 +0,0 @@ -validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], - 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], - ]); - - $validated['password'] = Hash::make($validated['password']); - - event(new Registered(($user = User::create($validated)))); - - Auth::login($user); - - $this->redirectIntended(route('dashboard', ['country' => str(session('lang_country', config('app.domain_country')))->after('-')->lower()],absolute: false), navigate: true); - } -}; ?> - -
- - - - - -
- - - - - - - - - - - - -
- - {{ __('Create account') }} - -
- - -
- {{ __('Already have an account?') }} - {{ __('Log in') }} -
-
diff --git a/routes/auth.php b/routes/auth.php index a12dee9..cdddf29 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -1,6 +1,7 @@ name('login'); - Route::livewire('/register', 'auth.register') - ->name('register'); - Route::livewire('/forgot-password', 'auth.forgot-password') ->name('password.request'); @@ -31,5 +29,5 @@ Route::middleware('auth') ->name('password.confirm'); }); -Route::post('logout', App\Livewire\Actions\Logout::class) +Route::post('logout', Logout::class) ->name('logout'); diff --git a/tests/Browser/AuthFlowTest.php b/tests/Browser/AuthFlowTest.php index c203132..3fceba1 100644 --- a/tests/Browser/AuthFlowTest.php +++ b/tests/Browser/AuthFlowTest.php @@ -7,9 +7,3 @@ it('renders the login page with QR code and language selector', function () { ->assertSee('Bitcoin, not blockchain') ->assertNoJavaScriptErrors(); }); - -it('renders the registration page', function () { - $page = visit('/register'); - - $page->assertNoJavaScriptErrors(); -}); diff --git a/tests/Browser/Cities/CityVenueCrudFlowTest.php b/tests/Browser/Cities/CityVenueCrudFlowTest.php new file mode 100644 index 0000000..9f6fdcf --- /dev/null +++ b/tests/Browser/Cities/CityVenueCrudFlowTest.php @@ -0,0 +1,67 @@ +country = Country::factory()->create(['code' => 'de', 'name' => 'Deutschland']); +}); + +it('creates a new city end-to-end with valid coordinates', function () { + actingAsUser(); + + $page = visit('/de/city-create'); + + $page->fill('[wire\\:model="name"]', 'BrowserTestCity') + ->fill('[wire\\:model="latitude"]', '52.520008') + ->fill('[wire\\:model="longitude"]', '13.404954') + ->click('[data-flux-button][type="submit"]') + ->wait(2) + ->assertNoJavaScriptErrors(); + + expect(City::query()->where('name', 'BrowserTestCity')->exists())->toBeTrue(); +}); + +it('shows validation errors when city form is submitted empty', function () { + actingAsUser(); + + $page = visit('/de/city-create'); + + $page->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(City::query()->count())->toBe(0); +}); + +it('creates a new venue connected to an existing city', function () { + actingAsUser(); + $city = City::factory()->create(['country_id' => $this->country->id, 'name' => 'VenueTestCity']); + + $page = visit('/de/venue-create'); + + $page->fill('[wire\\:model="name"]', 'BrowserTestVenue') + ->fill('[wire\\:model="street"]', 'Teststraße 1'); + $page->script("Livewire.getByName('venues.create')[0].set('city_id', {$city->id})"); + $page->wait(0.5) + ->click('[data-flux-button][type="submit"]') + ->wait(2) + ->assertNoJavaScriptErrors(); + + expect(Venue::query()->where('name', 'BrowserTestVenue')->exists())->toBeTrue(); +}); + +it('shows a validation error when creating a venue without a name', function () { + actingAsUser(); + City::factory()->create(['country_id' => $this->country->id]); + + $page = visit('/de/venue-create'); + + $page->fill('[wire\\:model="street"]', 'No-Name Street') + ->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(Venue::query()->count())->toBe(0); +}); diff --git a/tests/Browser/Lecturers/LecturerCrudFlowTest.php b/tests/Browser/Lecturers/LecturerCrudFlowTest.php new file mode 100644 index 0000000..225a1b8 --- /dev/null +++ b/tests/Browser/Lecturers/LecturerCrudFlowTest.php @@ -0,0 +1,58 @@ +fill('[wire\\:model="name"]', 'BrowserTester Saylor') + ->fill('[wire\\:model="subtitle"]', 'Browser Test Subject') + ->fill('[wire\\:model="intro"]', 'A short intro line.') + ->click('[data-flux-button][type="submit"]') + ->wait(2) + ->assertNoJavaScriptErrors(); + + expect(Lecturer::query()->where('name', 'BrowserTester Saylor')->exists())->toBeTrue(); +}); + +it('shows a required error when submitting without a lecturer name', function () { + actingAsUser(); + + $page = visit('/de/lecturer-create'); + + $page->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(Lecturer::query()->count())->toBe(0); +}); + +it('rejects creation when the lecturer name already exists', function () { + actingAsUser(); + Lecturer::factory()->create(['name' => 'Existing Saylor']); + + $page = visit('/de/lecturer-create'); + + $page->fill('[wire\\:model="name"]', 'Existing Saylor') + ->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(Lecturer::query()->where('name', 'Existing Saylor')->count())->toBe(1); +}); + +it('rejects creation with an invalid website URL', function () { + actingAsUser(); + + $page = visit('/de/lecturer-create'); + + $page->fill('[wire\\:model="name"]', 'Bad URL Lecturer') + ->fill('[wire\\:model="website"]', 'not-a-url') + ->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(Lecturer::query()->where('name', 'Bad URL Lecturer')->exists())->toBeFalse(); +}); diff --git a/tests/Browser/Meetups/MeetupCrudFlowTest.php b/tests/Browser/Meetups/MeetupCrudFlowTest.php new file mode 100644 index 0000000..50a8782 --- /dev/null +++ b/tests/Browser/Meetups/MeetupCrudFlowTest.php @@ -0,0 +1,60 @@ +country = Country::factory()->create(['code' => 'de', 'name' => 'Deutschland']); + $this->city = City::factory()->create([ + 'country_id' => $this->country->id, + 'name' => 'BrowserMeetupTestCity', + ]); +}); + +it('creates a new meetup end-to-end via the create form', function () { + actingAsUser(); + $cityId = $this->city->id; + + $page = visit('/de/meetup-create'); + + $page->fill('[wire\\:model="name"]', 'BrowserSeeded Meetup'); + $page->script("Livewire.getByName('meetups.create')[0].set('city_id', {$cityId})"); + $page->wait(0.5) + ->select('[wire\\:model="community"]', 'einundzwanzig'); + $page->script("Livewire.getByName('meetups.create')[0].call('createMeetup')"); + $page->wait(2) + ->assertNoJavaScriptErrors(); + + expect(Meetup::query()->where('name', 'BrowserSeeded Meetup')->exists())->toBeTrue(); +}); + +it('blocks meetup creation when the name is missing', function () { + actingAsUser(); + $cityId = $this->city->id; + + $page = visit('/de/meetup-create'); + + $page->script("Livewire.getByName('meetups.create')[0].set('city_id', {$cityId})"); + $page->wait(0.5) + ->select('[wire\\:model="community"]', 'einundzwanzig'); + $page->script("Livewire.getByName('meetups.create')[0].call('createMeetup')"); + $page->wait(1) + ->assertNoJavaScriptErrors(); + + expect(Meetup::query()->count())->toBe(0); +}); + +it('blocks meetup creation when no city is selected', function () { + actingAsUser(); + + $page = visit('/de/meetup-create'); + + $page->fill('[wire\\:model="name"]', 'NoCityMeetup') + ->select('[wire\\:model="community"]', 'einundzwanzig'); + $page->script("Livewire.getByName('meetups.create')[0].call('createMeetup')"); + $page->wait(1) + ->assertNoJavaScriptErrors(); + + expect(Meetup::query()->where('name', 'NoCityMeetup')->exists())->toBeFalse(); +}); diff --git a/tests/Browser/Public/ListFilteringTest.php b/tests/Browser/Public/ListFilteringTest.php new file mode 100644 index 0000000..98a7a2d --- /dev/null +++ b/tests/Browser/Public/ListFilteringTest.php @@ -0,0 +1,57 @@ +country = Country::factory()->create(['code' => 'de']); + $this->city = City::factory()->create(['country_id' => $this->country->id]); +}); + +it('renders all seeded services on the public services index', function () { + SelfHostedService::factory()->create(['name' => 'NodeAlpha', 'type' => SelfHostedServiceType::Mempool]); + SelfHostedService::factory()->create(['name' => 'BetaService', 'type' => SelfHostedServiceType::Other]); + + $page = visit('/de/services'); + + $page->assertSee('NodeAlpha') + ->assertSee('BetaService') + ->assertNoJavaScriptErrors(); +}); + +it('filters services by clicking a type-badge in the type-cloud', function () { + SelfHostedService::factory()->create(['name' => 'OnlyMempoolNode', 'type' => SelfHostedServiceType::Mempool]); + SelfHostedService::factory()->create(['name' => 'OnlyOtherThing', 'type' => SelfHostedServiceType::Other]); + + $page = visit('/de/services'); + + $page->assertSee('OnlyMempoolNode') + ->assertSee('OnlyOtherThing') + ->click('[wire\\:click="filterByType(\'mempool\')"]') + ->wait(1) + ->assertSee('OnlyMempoolNode') + ->assertDontSee('OnlyOtherThing') + ->assertNoJavaScriptErrors(); +}); + +it('shows seeded meetups on the public meetups index', function () { + Meetup::factory()->create([ + 'city_id' => $this->city->id, + 'name' => 'BrowserSeeded Meetup XYZ', + 'visible_on_map' => true, + ]); + + $page = visit('/de/meetups'); + + $page->assertSee('BrowserSeeded Meetup XYZ') + ->assertNoJavaScriptErrors(); +}); diff --git a/tests/Browser/Services/ServiceCrudFlowTest.php b/tests/Browser/Services/ServiceCrudFlowTest.php new file mode 100644 index 0000000..a9d92b6 --- /dev/null +++ b/tests/Browser/Services/ServiceCrudFlowTest.php @@ -0,0 +1,49 @@ +fill('[wire\\:model="form.name"]', 'BrowserTestNode') + ->select('[wire\\:model="form.type"]', SelfHostedServiceType::Mempool->value) + ->fill('[wire\\:model="form.url_clearnet"]', 'https://browsertest.example.com') + ->fill('[wire\\:model="form.intro"]', 'A node spun up by a browser test.') + ->click('[data-flux-button][type="submit"]') + ->wait(2) + ->assertNoJavaScriptErrors() + ->assertSee('BrowserTestNode'); + + expect(SelfHostedService::query()->where('name', 'BrowserTestNode')->exists())->toBeTrue(); +}); + +it('blocks submission without a name and shows a required error', function () { + actingAsUser(); + + $page = visit('/de/service-create'); + + $page->select('[wire\\:model="form.type"]', SelfHostedServiceType::Other->value) + ->fill('[wire\\:model="form.url_clearnet"]', 'https://no-name.example.com') + ->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(SelfHostedService::query()->count())->toBe(0); +}); + +it('rejects submission when no URL or IP is provided', function () { + actingAsUser(); + + $page = visit('/de/service-create'); + + $page->fill('[wire\\:model="form.name"]', 'NoUrlService') + ->select('[wire\\:model="form.type"]', SelfHostedServiceType::Other->value) + ->click('[data-flux-button][type="submit"]') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(SelfHostedService::query()->where('name', 'NoUrlService')->exists())->toBeFalse(); +}); diff --git a/tests/Browser/Settings/ProfileUpdateTest.php b/tests/Browser/Settings/ProfileUpdateTest.php new file mode 100644 index 0000000..ab0dab7 --- /dev/null +++ b/tests/Browser/Settings/ProfileUpdateTest.php @@ -0,0 +1,46 @@ + 'Old Name']); + + $page = visit('/de/settings/profile'); + + $page->assertSee('Old Name') + ->fill('name', 'New Browser Name') + ->click('Save') + ->wait(1) + ->assertSee('Saved.') + ->assertNoJavaScriptErrors(); + + expect($user->refresh()->name)->toBe('New Browser Name'); +}); + +it('shows a validation error when the profile name is cleared', function () { + actingAsUser(['name' => 'Original']); + + $page = visit('/de/settings/profile'); + + $page->fill('name', '') + ->click('Save') + ->wait(1) + ->assertNoJavaScriptErrors(); + + expect(User::query()->where('name', '')->exists())->toBeFalse(); +}); + +it('still shows the updated name after a full page reload', function () { + $user = actingAsUser(['name' => 'Before Reload']); + + $page = visit('/de/settings/profile'); + $page->fill('name', 'After Reload') + ->click('Save') + ->wait(1); + + $reloaded = visit('/de/settings/profile'); + $reloaded->assertSee('After Reload') + ->assertNoJavaScriptErrors(); + + expect($user->refresh()->name)->toBe('After Reload'); +}); diff --git a/tests/Browser/SmokeTest.php b/tests/Browser/SmokeTest.php index ce79a29..aff6b77 100644 --- a/tests/Browser/SmokeTest.php +++ b/tests/Browser/SmokeTest.php @@ -20,7 +20,6 @@ it('loads all listed public pages without console errors or JS errors', function $pages = visit([ '/welcome', '/login', - '/register', '/forgot-password', '/de/meetups', '/de/courses', diff --git a/tests/Feature/Livewire/AuthMountTest.php b/tests/Feature/Livewire/AuthMountTest.php index cf15f1a..b8a13ba 100644 --- a/tests/Feature/Livewire/AuthMountTest.php +++ b/tests/Feature/Livewire/AuthMountTest.php @@ -6,10 +6,6 @@ it('mounts the auth.login component', function () { Livewire::test('auth.login')->assertStatus(200); }); -it('mounts the auth.register component', function () { - Livewire::test('auth.register')->assertStatus(200); -}); - it('mounts the auth.forgot-password component', function () { Livewire::test('auth.forgot-password')->assertStatus(200); }); diff --git a/tests/Feature/Smoke/PublicRoutesTest.php b/tests/Feature/Smoke/PublicRoutesTest.php index 4bee343..07c21e5 100644 --- a/tests/Feature/Smoke/PublicRoutesTest.php +++ b/tests/Feature/Smoke/PublicRoutesTest.php @@ -25,7 +25,6 @@ it('returns a successful response for the listed public route', function (string })->with([ 'welcome' => '/welcome', 'login' => '/login', - 'register' => '/register', 'forgot password' => '/forgot-password', 'meetups index' => '/de/meetups', 'meetups all' => '/de/all-meetups', @@ -42,6 +41,10 @@ it('redirects / to /welcome', function () { $this->get('/')->assertRedirect('/welcome'); }); +it('returns 404 for /register because public registration is disabled', function () { + $this->get('/register')->assertNotFound(); +}); + it('redirects /de/dashboard to login when guest', function () { $this->get('/de/dashboard')->assertRedirect(route('login')); });