🔥 **Cleanup:** Removed obsolete .junie guideline files and MCP configuration.

 **Tests:** Added helper function `makeSignedNostrLoginEvent` for generating NIP-42 signed login events. Updated related tests in `Feature/Auth/NostrLoginTest.php` to use this helper.
🚀 **Livewire Testing:** Enhanced authorization checks and added specific creator-based mounts for `meetups.edit`. Improved tests for `MeetupMountTest` and `EditMeetupTest`.
🎨 **Style:** Standardized `request()->route()` to lowercase country codes across multiple Blade templates for consistency.
🛠️ **Config:** Updated `vite.config.js` formatting for improved readability in ignored paths.
This commit is contained in:
BT
2026-05-03 18:36:14 +02:00
parent cf330016a3
commit a4cbb10604
11 changed files with 116 additions and 523 deletions
+56 -9
View File
@@ -3,17 +3,61 @@
use App\Jobs\FetchNostrProfileJob;
use App\Models\User;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Session;
use Livewire\Livewire;
use swentel\nostr\Event\Event as NostrEvent;
use swentel\nostr\Key\Key as NostrKey;
use swentel\nostr\Sign\Sign as NostrSign;
/**
* Build a NIP-42-style signed login event using the challenge that the
* login component placed in the session during mount(), and return
* [signedEventArray, npubBech32].
*
* @return array{0: array<string, mixed>, 1: string}
*/
function makeSignedNostrLoginEvent(): array
{
$keyGen = new NostrKey;
$privateKey = $keyGen->generatePrivateKey();
$publicKey = $keyGen->getPublicKey($privateKey);
$challenge = Session::get('nostr_login_challenge');
$event = new NostrEvent;
$event->setKind(22242)
->setCreatedAt(time())
->setContent('')
->setTags([['challenge', (string) $challenge]]);
(new NostrSign)->signEvent($event, $privateKey);
$signed = [
'id' => $event->getId(),
'pubkey' => $event->getPublicKey(),
'created_at' => $event->getCreatedAt(),
'kind' => $event->getKind(),
'tags' => $event->getTags(),
'content' => $event->getContent(),
'sig' => $event->getSignature(),
];
$npub = $keyGen->convertPublicKeyToBech32($publicKey);
return [$signed, $npub];
}
it('creates a new user and dispatches FetchNostrProfileJob when an unknown pubkey logs in', function () {
Queue::fake();
$pubkey = 'npub1'.str_repeat('z', 58);
Livewire::test('auth.login')
->dispatch('nostrLoggedIn', pubkey: $pubkey)
$component = Livewire::test('auth.login');
[$signedEvent, $npub] = makeSignedNostrLoginEvent();
$component
->dispatch('nostrLoggedIn', signedEvent: $signedEvent)
->assertRedirect();
$user = User::query()->where('nostr', $pubkey)->first();
$user = User::query()->where('nostr', $npub)->first();
expect($user)->not->toBeNull()
->and((bool) $user->is_lecturer)->toBeTrue()
->and($user->email)->toEndWith('@portal.einundzwanzig.space');
@@ -24,14 +68,17 @@ it('creates a new user and dispatches FetchNostrProfileJob when an unknown pubke
it('logs in an existing user without creating a duplicate when their pubkey is already known', function () {
Queue::fake();
$pubkey = 'npub1'.str_repeat('a', 58);
$existing = User::factory()->create(['nostr' => $pubkey]);
Livewire::test('auth.login')
->dispatch('nostrLoggedIn', pubkey: $pubkey)
$component = Livewire::test('auth.login');
[$signedEvent, $npub] = makeSignedNostrLoginEvent();
$existing = User::factory()->create(['nostr' => $npub]);
$component
->dispatch('nostrLoggedIn', signedEvent: $signedEvent)
->assertRedirect();
expect(User::query()->where('nostr', $pubkey)->count())->toBe(1);
expect(User::query()->where('nostr', $npub)->count())->toBe(1);
expect(auth()->id())->toBe($existing->id);
Queue::assertPushed(FetchNostrProfileJob::class);
});
+2 -2
View File
@@ -43,8 +43,8 @@ it('rejects lecturer creation with invalid website URL', function () {
});
it('updates an existing lecturer', function () {
$lecturer = Lecturer::factory()->create(['name' => 'Old Name']);
actingAsUser();
$owner = actingAsUser();
$lecturer = Lecturer::factory()->create(['name' => 'Old Name', 'created_by' => $owner->id]);
Livewire::test('lecturers.edit', ['lecturer' => $lecturer])
->set('name', 'New Name')
+10 -2
View File
@@ -20,9 +20,17 @@ it('mounts lecturers.create when authenticated', function () {
Livewire::test('lecturers.create')->assertStatus(200);
});
it('mounts lecturers.edit when authenticated', function () {
it('mounts lecturers.edit when authenticated as the lecturer creator', function () {
$owner = actingAsUser();
$lecturer = Lecturer::factory()->create(['created_by' => $owner->id]);
Livewire::test('lecturers.edit', ['lecturer' => $lecturer])->assertStatus(200);
});
it('aborts lecturers.edit with 403 when authenticated user is not the creator', function () {
actingAsUser();
Livewire::test('lecturers.edit', ['lecturer' => $this->lecturer])->assertStatus(200);
Livewire::test('lecturers.edit', ['lecturer' => $this->lecturer])->assertStatus(403);
});
it('mounts cities.create when authenticated', function () {
+13 -2
View File
@@ -29,9 +29,20 @@ it('mounts meetups.create when authenticated', function () {
Livewire::test('meetups.create')->assertStatus(200);
});
it('mounts meetups.edit when authenticated', function () {
it('mounts meetups.edit when authenticated as the meetup creator', function () {
$owner = actingAsUser();
$meetup = Meetup::factory()->create([
'city_id' => $this->city->id,
'created_by' => $owner->id,
]);
Livewire::test('meetups.edit', ['meetup' => $meetup])->assertStatus(200);
});
it('aborts meetups.edit with 403 when authenticated user is not the creator', function () {
actingAsUser();
Livewire::test('meetups.edit', ['meetup' => $this->meetup])->assertStatus(200);
Livewire::test('meetups.edit', ['meetup' => $this->meetup])->assertStatus(403);
});
it('mounts meetups.create-edit-events for new event', function () {
+25 -9
View File
@@ -8,36 +8,50 @@ use Livewire\Livewire;
beforeEach(function () {
$country = Country::factory()->create(['code' => 'de']);
$this->city = City::factory()->create(['country_id' => $country->id]);
$this->meetup = Meetup::factory()->create(['city_id' => $this->city->id, 'name' => 'Original Name']);
});
it('updates an existing Meetup name when authenticated', function () {
actingAsUser();
$owner = actingAsUser();
$meetup = Meetup::factory()->create([
'city_id' => $this->city->id,
'name' => 'Original Name',
'created_by' => $owner->id,
]);
Livewire::test('meetups.edit', ['meetup' => $this->meetup])
Livewire::test('meetups.edit', ['meetup' => $meetup])
->set('name', 'Updated Name')
->set('city_id', $this->city->id)
->set('community', 'einundzwanzig')
->call('updateMeetup')
->assertHasNoErrors();
expect($this->meetup->refresh()->name)->toBe('Updated Name');
expect($meetup->refresh()->name)->toBe('Updated Name');
});
it('rejects update when name collides with another existing Meetup', function () {
$owner = actingAsUser();
$meetup = Meetup::factory()->create([
'city_id' => $this->city->id,
'name' => 'Original Name',
'created_by' => $owner->id,
]);
Meetup::factory()->create(['name' => 'Other Name', 'city_id' => $this->city->id]);
actingAsUser();
Livewire::test('meetups.edit', ['meetup' => $this->meetup])
Livewire::test('meetups.edit', ['meetup' => $meetup])
->set('name', 'Other Name')
->call('updateMeetup')
->assertHasErrors(['name' => 'unique']);
});
it('allows update when name is unchanged (Rule::unique ignores own id)', function () {
actingAsUser();
$owner = actingAsUser();
$meetup = Meetup::factory()->create([
'city_id' => $this->city->id,
'name' => 'Original Name',
'created_by' => $owner->id,
]);
Livewire::test('meetups.edit', ['meetup' => $this->meetup])
Livewire::test('meetups.edit', ['meetup' => $meetup])
->set('name', 'Original Name')
->set('community', 'einundzwanzig')
->call('updateMeetup')
@@ -45,5 +59,7 @@ it('allows update when name is unchanged (Rule::unique ignores own id)', functio
});
it('redirects guests when accessing meetup-edit', function () {
$this->get('/de/meetup-edit/'.$this->meetup->id)->assertRedirect(route('login'));
$meetup = Meetup::factory()->create(['city_id' => $this->city->id]);
$this->get('/de/meetup-edit/'.$meetup->id)->assertRedirect(route('login'));
});
+2 -2
View File
@@ -46,8 +46,8 @@ it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function (
$this->get('/api/meetup/ical')->assertNotFound();
});
it('returns 404 for /api/meetup index without user_id (currently aborts on missing param)', function () {
$this->getJson('/api/meetup')->assertNotFound();
it('returns 401 for /api/meetup index when unauthenticated (auth-only after IDOR fix)', function () {
$this->getJson('/api/meetup')->assertUnauthorized();
});
it('returns a successful response for /stream-calendar', function () {