🔥 **Tests:** Removed obsolete feature tests for deleted components and endpoints across the project.

This commit is contained in:
BT
2026-05-02 19:59:16 +01:00
parent ef3c06acb9
commit 63aed880e1
9 changed files with 14 additions and 824 deletions
Binary file not shown.
@@ -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;
+9
View File
@@ -0,0 +1,9 @@
<?php
use Livewire\Livewire;
it('rejects non-string updates to currentCountry to prevent TypeError on string-typed property', function () {
Livewire::test('country.chooser')
->set('currentCountry', [])
->assertStatus(422);
});
@@ -1,195 +0,0 @@
<?php
use App\Enums\RecurrenceType;
use App\Models\City;
use App\Models\Country;
use App\Models\Meetup;
use App\Models\User;
use Livewire\Livewire;
use function Pest\Laravel\assertDatabaseCount;
beforeEach(function () {
$this->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');
});
@@ -1,65 +0,0 @@
<?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);
});
-232
View File
@@ -1,232 +0,0 @@
<?php
use App\Http\Controllers\Api\HighscoreController;
use App\Models\Highscore;
use Carbon\CarbonImmutable;
test('highscore submission is accepted and stored', function () {
$payload = [
'npub' => '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');
@@ -1,132 +0,0 @@
<?php
use App\Models\City;
use App\Models\Country;
use App\Models\Meetup;
use App\Models\SelfHostedService;
use App\Models\User;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Livewire\Livewire;
beforeEach(function () {
$this->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);
});
});
-139
View File
@@ -1,139 +0,0 @@
<?php
use App\Models\LoginKey;
use App\Models\User;
beforeEach(function () {
LoginKey::query()->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]);
});
-61
View File
@@ -1,61 +0,0 @@
<?php
use App\Models\City;
use App\Models\Country;
use App\Models\Meetup;
use App\Models\MeetupEvent;
it('displays event time 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, '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');
});