mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-05-03 16:24:55 +00:00
🔥 **Tests:** Removed obsolete feature tests for deleted components and endpoints across the project.
This commit is contained in:
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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user