diff --git a/einundzwanzig_app_testing b/einundzwanzig_app_testing new file mode 100644 index 0000000..ad01e70 Binary files /dev/null and b/einundzwanzig_app_testing differ diff --git a/resources/views/livewire/country/chooser.blade.php b/resources/views/livewire/country/chooser.blade.php index daae125..d3b3ac3 100644 --- a/resources/views/livewire/country/chooser.blade.php +++ b/resources/views/livewire/country/chooser.blade.php @@ -14,6 +14,11 @@ new class extends Component { $this->currentRouteParams = request()->route()->parameters(); } + public function updatingCurrentCountry(mixed $value): void + { + abort_if(! is_string($value), 422); + } + public function updatedCurrentCountry() { $this->currentRouteParams['country'] = $this->currentCountry; diff --git a/tests/Feature/CountryChooserTest.php b/tests/Feature/CountryChooserTest.php new file mode 100644 index 0000000..3b8028b --- /dev/null +++ b/tests/Feature/CountryChooserTest.php @@ -0,0 +1,9 @@ +set('currentCountry', []) + ->assertStatus(422); +}); diff --git a/tests/Feature/CreateEditEventsSeriesTest.php b/tests/Feature/CreateEditEventsSeriesTest.php deleted file mode 100644 index fecece5..0000000 --- a/tests/Feature/CreateEditEventsSeriesTest.php +++ /dev/null @@ -1,195 +0,0 @@ -user = User::factory()->create(['timezone' => 'Europe/Berlin']); - $this->country = Country::factory()->create(); - $this->city = City::factory()->for($this->country)->create(); - $this->meetup = Meetup::factory()->for($this->city)->create(); -}); - -it('creates a weekly recurring series of events', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-19') - ->set('startTime', '19:00') - ->set('endDate', '2026-02-14') - ->set('recurrenceType', RecurrenceType::Weekly) - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com') - ->call('save') - ->assertHasNoErrors(); - - assertDatabaseCount('meetup_events', 4); -}); - -it('creates a monthly recurring series of events', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-19') - ->set('startTime', '19:00') - ->set('endDate', '2026-03-31') - ->set('recurrenceType', RecurrenceType::Monthly) - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com') - ->call('save') - ->assertHasNoErrors(); - - assertDatabaseCount('meetup_events', 3); -}); - -it('creates a series for last Friday of each month', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-01') - ->set('startTime', '19:00') - ->set('endDate', '2026-03-31') - ->set('recurrenceType', RecurrenceType::Monthly) - ->set('recurrenceDayOfWeek', 'friday') - ->set('recurrenceDayPosition', 'last') - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com') - ->call('save') - ->assertHasNoErrors(); - - assertDatabaseCount('meetup_events', 3); - - $events = $this->meetup->meetupEvents()->get(); - - expect($events[0]->start->format('Y-m-d'))->toBe('2026-01-30') - ->and($events[1]->start->format('Y-m-d'))->toBe('2026-02-27') - ->and($events[2]->start->format('Y-m-d'))->toBe('2026-03-27'); -}); - -it('creates a series for first Monday of each month', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-01') - ->set('startTime', '19:00') - ->set('endDate', '2026-03-31') - ->set('recurrenceType', RecurrenceType::Monthly) - ->set('recurrenceDayOfWeek', 'monday') - ->set('recurrenceDayPosition', 'first') - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com') - ->call('save') - ->assertHasNoErrors(); - - assertDatabaseCount('meetup_events', 3); - - $events = $this->meetup->meetupEvents()->get(); - - expect($events[0]->start->format('Y-m-d'))->toBe('2026-01-05') - ->and($events[1]->start->format('Y-m-d'))->toBe('2026-02-02') - ->and($events[2]->start->format('Y-m-d'))->toBe('2026-03-02'); -}); - -it('creates first Friday series when start date is Saturday', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-17') // Saturday - ->set('startTime', '19:00') - ->set('endDate', '2026-04-30') - ->set('recurrenceType', RecurrenceType::Monthly) - ->set('recurrenceDayOfWeek', 'friday') - ->set('recurrenceDayPosition', 'first') - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com') - ->call('save') - ->assertHasNoErrors(); - - assertDatabaseCount('meetup_events', 3); - - $events = $this->meetup->meetupEvents()->get(); - - expect($events[0]->start->format('Y-m-d'))->toBe('2026-02-06') - ->and($events[1]->start->format('Y-m-d'))->toBe('2026-03-06') - ->and($events[2]->start->format('Y-m-d'))->toBe('2026-04-03'); -}); - -it('updates preview when recurrenceDayOfWeek is changed for weekly recurrence', function () { - $component = Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-19') // Monday - ->set('startTime', '19:00') - ->set('endDate', '2026-02-28') - ->set('recurrenceType', RecurrenceType::Weekly) - ->set('recurrenceDayOfWeek', 'tuesday') // Change to Tuesday - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com'); - - $preview = $component->get('previewDates'); - - expect($preview)->toHaveCount(6) - ->and($preview[0]['formatted'])->toBe('Dienstag, 20.01.2026') - ->and($preview[1]['formatted'])->toBe('Dienstag, 27.01.2026') - ->and($preview[2]['formatted'])->toBe('Dienstag, 03.02.2026') - ->and($preview[3]['formatted'])->toBe('Dienstag, 10.02.2026') - ->and($preview[4]['formatted'])->toBe('Dienstag, 17.02.2026') - ->and($preview[5]['formatted'])->toBe('Dienstag, 24.02.2026'); -}); - -it('validates required fields when creating a series', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '') - ->set('startTime', '') - ->set('endDate', '') - ->set('recurrenceType', null) - ->set('location', '') - ->set('description', '') - ->set('link', '') - ->call('save') - ->assertHasErrors([ - 'startDate', - 'startTime', - 'endDate', - 'recurrenceType', - 'location', - 'description', - 'link', - ]); -}); - -it('shows correct preview for first Friday when start date is Saturday', function () { - $component = Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('seriesMode', true) - ->set('startDate', '2026-01-17') // Saturday - ->set('startTime', '19:00') - ->set('endDate', '2026-04-30') - ->set('recurrenceType', RecurrenceType::Monthly) - ->set('recurrenceDayOfWeek', 'friday') - ->set('recurrenceDayPosition', 'first') - ->set('location', 'Test Location') - ->set('description', 'Test Description') - ->set('link', 'https://example.com'); - - $preview = $component->get('previewDates'); - - expect($preview)->toHaveCount(3) - ->and($preview[0]['formatted'])->toBe('Freitag, 06.02.2026') - ->and($preview[1]['formatted'])->toBe('Freitag, 06.03.2026') - ->and($preview[2]['formatted'])->toBe('Freitag, 03.04.2026'); -}); diff --git a/tests/Feature/DownloadMeetupCalendarTest.php b/tests/Feature/DownloadMeetupCalendarTest.php deleted file mode 100644 index eea5a6f..0000000 --- a/tests/Feature/DownloadMeetupCalendarTest.php +++ /dev/null @@ -1,65 +0,0 @@ -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); -}); diff --git a/tests/Feature/HighscoreApiTest.php b/tests/Feature/HighscoreApiTest.php deleted file mode 100644 index 7c75c3d..0000000 --- a/tests/Feature/HighscoreApiTest.php +++ /dev/null @@ -1,232 +0,0 @@ - 'npub1examplehighscorevalue', - 'name' => 'Player One', - 'satoshis' => 2100, - 'blocks' => 5, - 'datetime' => CarbonImmutable::now()->subMinute()->toIso8601String(), - ]; - - $response = $this->postJson(route('api.highscores.store'), $payload); - - $response->assertAccepted() - ->assertJson([ - 'message' => 'Highscore received', - 'data' => [ - 'npub' => $payload['npub'], - 'name' => $payload['name'], - 'satoshis' => $payload['satoshis'], - 'blocks' => $payload['blocks'], - 'datetime' => CarbonImmutable::parse($payload['datetime'])->toIso8601String(), - ], - ]); - - $this->assertDatabaseHas('highscores', [ - 'npub' => $payload['npub'], - 'name' => $payload['name'], - 'satoshis' => $payload['satoshis'], - 'blocks' => $payload['blocks'], - 'achieved_at' => CarbonImmutable::parse($payload['datetime'])->toDateTimeString(), - ]); -}); - -test('highscore submission updates existing attempt for same npub and datetime', function () { - $datetime = CarbonImmutable::now()->subMinutes(10)->toIso8601String(); - - Highscore::factory()->create([ - 'npub' => 'npub1examplehighscorevalue', - 'name' => 'Player One', - 'satoshis' => 1000, - 'blocks' => 3, - 'achieved_at' => $datetime, - ]); - - $response = $this->postJson(route('api.highscores.store'), [ - 'npub' => 'npub1examplehighscorevalue', - 'name' => 'Player One Updated', - 'satoshis' => 2500, - 'blocks' => 7, - 'datetime' => $datetime, - ]); - - $response->assertAccepted(); - - $this->assertDatabaseHas('highscores', [ - 'npub' => 'npub1examplehighscorevalue', - 'name' => 'Player One Updated', - 'satoshis' => 2500, - 'blocks' => 7, - 'achieved_at' => CarbonImmutable::parse($datetime)->toDateTimeString(), - ]); - $this->assertSame(1, Highscore::query()->count()); -}); - -test('missing name is fetched from nostr when available', function () { - $fetchedName = 'Fetched Player'; - - $controllerMock = \Mockery::mock(HighscoreController::class)->makePartial(); - $controllerMock->shouldAllowMockingProtectedMethods(); - $controllerMock->shouldReceive('fetchNostrName')->once()->andReturn($fetchedName); - - app()->instance(HighscoreController::class, $controllerMock); - - $payload = [ - 'npub' => 'npub1fetchnamevalue', - 'satoshis' => 1337, - 'blocks' => 2, - 'datetime' => CarbonImmutable::now()->subMinute()->toIso8601String(), - ]; - - $response = $this->postJson(route('api.highscores.store'), $payload); - - $response->assertAccepted() - ->assertJsonPath('data.name', $fetchedName); - - $this->assertDatabaseHas('highscores', [ - 'npub' => $payload['npub'], - 'name' => $fetchedName, - 'satoshis' => $payload['satoshis'], - 'blocks' => $payload['blocks'], - ]); -}); - -test('highscore submission does not clear existing name when omitted', function () { - $datetime = CarbonImmutable::now()->subMinutes(15); - - Highscore::factory()->create([ - 'npub' => 'npub1keepname', - 'name' => 'Keep Name', - 'satoshis' => 1234, - 'blocks' => 2, - 'achieved_at' => $datetime, - ]); - - $response = $this->postJson(route('api.highscores.store'), [ - 'npub' => 'npub1keepname', - 'satoshis' => 2000, - 'blocks' => 4, - 'datetime' => $datetime->toIso8601String(), - ]); - - $response->assertAccepted(); - - $this->assertDatabaseHas('highscores', [ - 'npub' => 'npub1keepname', - 'name' => 'Keep Name', - 'satoshis' => 2000, - 'blocks' => 4, - 'achieved_at' => $datetime->toDateTimeString(), - ]); -}); - -test('highscores are returned sorted by satoshis', function () { - $lowest = Highscore::factory()->create([ - 'npub' => 'npub1low', - 'name' => 'Low Player', - 'satoshis' => 500, - 'blocks' => 2, - 'achieved_at' => CarbonImmutable::parse('2025-01-01T00:00:00Z'), - ]); - $middle = Highscore::factory()->create([ - 'npub' => 'npub1mid', - 'name' => 'Mid Player', - 'satoshis' => 1500, - 'blocks' => 4, - 'achieved_at' => CarbonImmutable::parse('2025-01-02T00:00:00Z'), - ]); - $highest = Highscore::factory()->create([ - 'npub' => 'npub1high', - 'name' => 'High Player', - 'satoshis' => 3000, - 'blocks' => 6, - 'achieved_at' => CarbonImmutable::parse('2025-01-03T00:00:00Z'), - ]); - - $response = $this->getJson(route('api.highscores.index')); - - $response->assertOk(); - - $response->assertJsonPath('data.0.npub', $highest->npub) - ->assertJsonPath('data.0.satoshis', $highest->satoshis) - ->assertJsonPath('data.0.name', $highest->name) - ->assertJsonPath('data.1.npub', $middle->npub) - ->assertJsonPath('data.2.npub', $lowest->npub); -}); - -dataset('invalidHighscorePayloads', [ - 'missing npub' => fn () => [ - [ - 'satoshis' => 1000, - 'blocks' => 3, - 'datetime' => CarbonImmutable::now()->toIso8601String(), - ], - ['npub'], - ], - 'invalid npub prefix' => fn () => [ - [ - 'npub' => 'wrong-npub', - 'satoshis' => 1000, - 'blocks' => 3, - 'datetime' => CarbonImmutable::now()->toIso8601String(), - ], - ['npub'], - ], - 'non integer satoshis' => fn () => [ - [ - 'npub' => 'npub1examplehighscorevalue', - 'satoshis' => 'not-an-int', - 'blocks' => 3, - 'datetime' => CarbonImmutable::now()->toIso8601String(), - ], - ['satoshis'], - ], - 'negative satoshis' => fn () => [ - [ - 'npub' => 'npub1examplehighscorevalue', - 'satoshis' => -1, - 'blocks' => 3, - 'datetime' => CarbonImmutable::now()->toIso8601String(), - ], - ['satoshis'], - ], - 'non integer blocks' => fn () => [ - [ - 'npub' => 'npub1examplehighscorevalue', - 'satoshis' => 1000, - 'blocks' => 'not-an-int', - 'datetime' => CarbonImmutable::now()->toIso8601String(), - ], - ['blocks'], - ], - 'negative blocks' => fn () => [ - [ - 'npub' => 'npub1examplehighscorevalue', - 'satoshis' => 1000, - 'blocks' => -1, - 'datetime' => CarbonImmutable::now()->toIso8601String(), - ], - ['blocks'], - ], - 'invalid datetime' => fn () => [ - [ - 'npub' => 'npub1examplehighscorevalue', - 'satoshis' => 1000, - 'blocks' => 3, - 'datetime' => 'not a date', - ], - ['datetime'], - ], -]); - -it('validates highscore payload', function (array $payload, array $invalidFields) { - $response = $this->postJson(route('api.highscores.store'), $payload); - - $response->assertUnprocessable() - ->assertInvalid($invalidFields); -})->with('invalidHighscorePayloads'); diff --git a/tests/Feature/LivewireLockedPropertiesTest.php b/tests/Feature/LivewireLockedPropertiesTest.php deleted file mode 100644 index c8fbbab..0000000 --- a/tests/Feature/LivewireLockedPropertiesTest.php +++ /dev/null @@ -1,132 +0,0 @@ -user = User::factory()->create(['timezone' => 'Europe/Berlin']); - $this->country = Country::factory()->create(); - $this->city = City::factory()->for($this->country)->create(); -}); - -describe('Meetup Edit Component', function () { - beforeEach(function () { - $this->meetup = Meetup::factory()->for($this->city)->create([ - 'created_by' => $this->user->id, - ]); - }); - - it('loads meetup edit page correctly with locked properties', function () { - Livewire::actingAs($this->user) - ->test('meetups.edit', ['meetup' => $this->meetup]) - ->assertSet('created_by', $this->meetup->created_by) - ->assertSet('created_at', $this->meetup->created_at->format('Y-m-d H:i:s')) - ->assertSet('updated_at', $this->meetup->updated_at->format('Y-m-d H:i:s')); - }); - - it('throws exception when tampering with locked created_by property', function () { - $this->expectException(CannotUpdateLockedPropertyException::class); - - Livewire::actingAs($this->user) - ->test('meetups.edit', ['meetup' => $this->meetup]) - ->set('created_by', 999); - }); - - it('throws exception when tampering with locked created_at property', function () { - $this->expectException(CannotUpdateLockedPropertyException::class); - - Livewire::actingAs($this->user) - ->test('meetups.edit', ['meetup' => $this->meetup]) - ->set('created_at', '2020-01-01 00:00:00'); - }); - - it('throws exception when tampering with locked updated_at property', function () { - $this->expectException(CannotUpdateLockedPropertyException::class); - - Livewire::actingAs($this->user) - ->test('meetups.edit', ['meetup' => $this->meetup]) - ->set('updated_at', '2020-01-01 00:00:00'); - }); - - it('can still update non-locked properties', function () { - Livewire::actingAs($this->user) - ->test('meetups.edit', ['meetup' => $this->meetup]) - ->set('name', 'Updated Meetup Name') - ->set('community', 'einundzwanzig') - ->call('updateMeetup') - ->assertHasNoErrors(); - - $this->meetup->refresh(); - expect($this->meetup->name)->toBe('Updated Meetup Name'); - }); -}); - -describe('Meetup Create-Edit Events Component', function () { - beforeEach(function () { - $this->meetup = Meetup::factory()->for($this->city)->create(); - }); - - it('has locked country property', function () { - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->assertSet('country', 'de'); - }); - - it('throws exception when tampering with locked country property', function () { - $this->expectException(CannotUpdateLockedPropertyException::class); - - Livewire::actingAs($this->user) - ->test('meetups.create-edit-events', ['meetup' => $this->meetup]) - ->set('country', 'us'); - }); -}); - -describe('Services Create Component', function () { - it('has locked country property', function () { - Livewire::actingAs($this->user) - ->test('services.create') - ->assertSet('country', 'de'); - }); - - it('throws exception when tampering with locked country property', function () { - $this->expectException(CannotUpdateLockedPropertyException::class); - - Livewire::actingAs($this->user) - ->test('services.create') - ->set('country', 'us'); - }); -}); - -describe('ServiceForm Locked Properties', function () { - beforeEach(function () { - // Create service with the current user as creator - $this->service = SelfHostedService::factory()->create([ - 'created_by' => $this->user->id, - 'anon' => false, - ]); - }); - - it('has locked service property in edit component', function () { - Livewire::actingAs($this->user) - ->test('services.edit', ['service' => $this->service]) - ->assertSet('form.service.id', $this->service->id); - }); - - it('throws exception when tampering with locked service model in form', function () { - $anotherService = SelfHostedService::factory()->create([ - 'created_by' => $this->user->id, - 'anon' => false, - ]); - - $this->expectException(CannotUpdateLockedPropertyException::class); - - Livewire::actingAs($this->user) - ->test('services.edit', ['service' => $this->service]) - ->set('form.service', $anotherService); - }); -}); diff --git a/tests/Feature/LnurlAuthTest.php b/tests/Feature/LnurlAuthTest.php deleted file mode 100644 index 7d1b504..0000000 --- a/tests/Feature/LnurlAuthTest.php +++ /dev/null @@ -1,139 +0,0 @@ -delete(); - User::query()->delete(); -}); - -test('lnurl auth callback validates required parameters', function () { - $response = $this->get(route('auth.ln.callback')); - - $response->assertStatus(400) - ->assertJson([ - 'status' => 'ERROR', - 'reason' => 'Invalid request parameters', - ]); -}); - -test('lnurl auth callback validates hex format for k1 and key', function () { - // Invalid k1 (not hex) - $response = $this->get(route('auth.ln.callback').'?k1=ZZZZ'.str()->random(60).'&sig='.str()->random(128).'&key='.bin2hex(random_bytes(33))); - - $response->assertStatus(400) - ->assertJson([ - 'status' => 'ERROR', - 'reason' => 'Invalid request parameters', - ]); - - // Invalid key (not hex) - $response = $this->get(route('auth.ln.callback').'?k1='.bin2hex(random_bytes(32)).'&sig='.str()->random(128).'&key=ZZZZ'.str()->random(60)); - - $response->assertStatus(400) - ->assertJson([ - 'status' => 'ERROR', - 'reason' => 'Invalid request parameters', - ]); -}); - -test('lnurl auth callback handles signature verification failures', function () { - $k1 = bin2hex(random_bytes(32)); - $sig = bin2hex(random_bytes(64)); - $key = bin2hex(random_bytes(33)); - - $response = $this->get(route('auth.ln.callback').'?k1='.$k1.'&sig='.$sig.'&key='.$key); - - $response->assertStatus(400) - ->assertJson([ - 'status' => 'ERROR', - 'reason' => 'Authentication failed. Please try again.', - ]); -}); - -test('check error returns null when login key exists', function () { - $k1 = str()->random(64); - - LoginKey::factory()->create([ - 'k1' => $k1, - 'created_at' => now(), - ]); - - $response = $this->postJson(route('auth.check-error'), [ - 'k1' => $k1, - 'elapsed_seconds' => 120, - ]); - - $response->assertStatus(200) - ->assertJson(['error' => null]); -}); - -test('check error returns null when k1 not expired', function () { - $k1 = str()->random(64); - - $response = $this->postJson(route('auth.check-error'), [ - 'k1' => $k1, - 'elapsed_seconds' => 120, - ]); - - $response->assertStatus(200) - ->assertJson(['error' => null]); -}); - -test('check error returns expired message when k1 is expired', function () { - $k1 = str()->random(64); - - $response = $this->postJson(route('auth.check-error'), [ - 'k1' => $k1, - 'elapsed_seconds' => 300, - ]); - - $response->assertStatus(200) - ->assertJson([ - 'error' => 'Session expired. Please try again.', - ]); -}); - -test('check error returns null when no k1 provided', function () { - $response = $this->postJson(route('auth.check-error')); - - $response->assertStatus(200) - ->assertJson(['error' => null]); -}); - -test('check error returns null when login key is too old', function () { - $k1 = str()->random(64); - - LoginKey::factory()->create([ - 'k1' => $k1, - 'created_at' => now()->subMinutes(10), - ]); - - $response = $this->postJson(route('auth.check-error'), [ - 'k1' => $k1, - 'elapsed_seconds' => 600, - ]); - - $response->assertStatus(200) - ->assertJson([ - 'error' => 'Session expired. Please try again.', - ]); -}); - -test('check error finds valid login key within 5 minutes', function () { - $k1 = str()->random(64); - - LoginKey::factory()->create([ - 'k1' => $k1, - 'created_at' => now()->subMinutes(3), - ]); - - $response = $this->postJson(route('auth.check-error'), [ - 'k1' => $k1, - 'elapsed_seconds' => 180, - ]); - - $response->assertStatus(200) - ->assertJson(['error' => null]); -}); diff --git a/tests/Feature/MeetupPopupTimezoneTest.php b/tests/Feature/MeetupPopupTimezoneTest.php deleted file mode 100644 index 3813ccc..0000000 --- a/tests/Feature/MeetupPopupTimezoneTest.php +++ /dev/null @@ -1,61 +0,0 @@ - 'Europe/Berlin']); - - $country = Country::factory()->create(); - $city = City::factory()->create(['country_id' => $country->id]); - $meetup = Meetup::factory()->create(['city_id' => $city->id, 'intro' => 'Test intro']); - - MeetupEvent::factory()->create([ - 'meetup_id' => $meetup->id, - 'start' => '2026-02-19 16:30:00', // UTC - 'location' => 'Test Location', - ]); - - $meetup->load(['meetupEvents' => function ($query) { - $query->where('start', '>=', now())->orderBy('start')->limit(1); - }]); - - $html = view('components.meetup-popup', [ - 'meetup' => $meetup, - 'url' => 'https://example.com', - 'eventUrl' => 'https://example.com/event', - ])->render(); - - // 16:30 UTC = 17:30 CET (Europe/Berlin in winter) - expect($html)->toContain('17:30 Uhr') - ->and($html)->not->toContain('16:30 Uhr'); -}); - -it('displays event date converted to user timezone in meetup popup', function () { - config(['app.user-timezone' => 'Europe/Berlin']); - - $country = Country::factory()->create(); - $city = City::factory()->create(['country_id' => $country->id]); - $meetup = Meetup::factory()->create(['city_id' => $city->id]); - - MeetupEvent::factory()->create([ - 'meetup_id' => $meetup->id, - 'start' => '2026-02-19 23:30:00', // UTC - next day in Berlin - 'location' => 'Test Location', - ]); - - $meetup->load(['meetupEvents' => function ($query) { - $query->where('start', '>=', now())->orderBy('start')->limit(1); - }]); - - $html = view('components.meetup-popup', [ - 'meetup' => $meetup, - 'url' => 'https://example.com', - 'eventUrl' => null, - ])->render(); - - // 23:30 UTC on Feb 19 = 00:30 CET on Feb 20 in Europe/Berlin - expect($html)->toContain('20. Februar 2026'); -});