The portal store merged events from the API with localStorage events without ever pruning stale entries. When /auth/me returned empty portal_events (e.g. after a person's user_id was cleared), localStorage events persisted, causing "registratie niet ophalen" when /portal/me correctly returned 404. Now when /auth/me succeeds, API data is the source of truth — stored events not confirmed by the API are dropped. localStorage fallback is only used when the API call fails (network error). Also adds an end-to-end test covering the full register → approve → portal/me flow including festival hierarchy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
293 lines
10 KiB
PHP
293 lines
10 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Tests\Feature\Api\V1;
|
||
|
||
use App\Models\CrowdType;
|
||
use App\Models\Event;
|
||
use App\Models\Organisation;
|
||
use App\Models\Person;
|
||
use App\Models\User;
|
||
use Database\Seeders\RoleSeeder;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Laravel\Sanctum\Sanctum;
|
||
use Tests\TestCase;
|
||
|
||
/**
|
||
* End-to-end test: public registration → organizer approval → portal login → portal/me
|
||
*
|
||
* Reproduces: "Portal shows 'registratie niet ophalen' after approved volunteer logs in"
|
||
*/
|
||
class PortalRegistrationFlowTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
private Organisation $organisation;
|
||
private Event $event;
|
||
private CrowdType $volunteerCrowdType;
|
||
private User $orgAdmin;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
$this->seed(RoleSeeder::class);
|
||
|
||
$this->organisation = Organisation::factory()->create();
|
||
$this->volunteerCrowdType = CrowdType::factory()->systemType('VOLUNTEER')->create([
|
||
'organisation_id' => $this->organisation->id,
|
||
]);
|
||
$this->event = Event::factory()->create([
|
||
'organisation_id' => $this->organisation->id,
|
||
'status' => 'registration_open',
|
||
]);
|
||
|
||
// Create org admin for approval
|
||
$this->orgAdmin = User::factory()->create();
|
||
$this->orgAdmin->assignRole('super_admin');
|
||
$this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']);
|
||
}
|
||
|
||
/**
|
||
* The golden path: register → approve → portal/me works.
|
||
*/
|
||
public function test_full_flow_register_approve_portal_me(): void
|
||
{
|
||
Mail::fake();
|
||
|
||
// ── Step 1: Volunteer registers via public form ──
|
||
$regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
|
||
'first_name' => 'Vrijwilliger',
|
||
'last_name' => 'Test',
|
||
'email' => 'vrijwilliger@test.nl',
|
||
'password' => 'Wachtwoord1',
|
||
]);
|
||
|
||
$regResponse->assertStatus(201);
|
||
|
||
// Verify person was created with user_id
|
||
$person = Person::where('email', 'vrijwilliger@test.nl')->first();
|
||
$this->assertNotNull($person, 'Person record should exist');
|
||
$this->assertNotNull($person->user_id, 'Person should have user_id after registration');
|
||
$this->assertEquals('pending', $person->status);
|
||
|
||
$user = User::where('email', 'vrijwilliger@test.nl')->first();
|
||
$this->assertNotNull($user, 'User account should exist');
|
||
$this->assertEquals($user->id, $person->user_id, 'Person.user_id should match created user');
|
||
|
||
// ── Step 2: Organizer approves the volunteer ─<><E29480><EFBFBD>
|
||
Sanctum::actingAs($this->orgAdmin);
|
||
|
||
$approveResponse = $this->postJson(
|
||
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve"
|
||
);
|
||
|
||
$approveResponse->assertOk();
|
||
|
||
// Verify person is now approved
|
||
$person->refresh();
|
||
$this->assertEquals('approved', $person->status);
|
||
$this->assertNotNull($person->user_id, 'user_id should still be set after approval');
|
||
|
||
// ── Step 3: Volunteer logs into portal ──
|
||
Sanctum::actingAs($user);
|
||
|
||
// Step 3a: GET /auth/me should return portal_events
|
||
$meResponse = $this->getJson('/api/v1/auth/me');
|
||
|
||
$meResponse->assertOk();
|
||
$meResponse->assertJsonCount(1, 'data.portal_events');
|
||
$meResponse->assertJsonPath('data.portal_events.0.event_id', $this->event->id);
|
||
$meResponse->assertJsonPath('data.portal_events.0.person_status', 'approved');
|
||
|
||
// Step 3b: GET /portal/me should return person details
|
||
$portalMeResponse = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}");
|
||
|
||
$portalMeResponse->assertOk();
|
||
$portalMeResponse->assertJsonPath('data.email', 'vrijwilliger@test.nl');
|
||
$portalMeResponse->assertJsonPath('data.status', 'approved');
|
||
}
|
||
|
||
/**
|
||
* Festival hierarchy: register via sub-event slug → portal/me with parent event_id.
|
||
*/
|
||
public function test_full_flow_with_festival_sub_event(): void
|
||
{
|
||
Mail::fake();
|
||
|
||
// Create festival hierarchy
|
||
$festival = Event::factory()->festival()->create([
|
||
'organisation_id' => $this->organisation->id,
|
||
'status' => 'registration_open',
|
||
]);
|
||
$subEvent = Event::factory()->subEvent($festival)->create([
|
||
'status' => 'registration_open',
|
||
]);
|
||
|
||
// ── Step 1: Register via sub-event route ──
|
||
$regResponse = $this->postJson("/api/v1/events/{$subEvent->id}/volunteer-register", [
|
||
'first_name' => 'Festival',
|
||
'last_name' => 'Ganger',
|
||
'email' => 'festival@test.nl',
|
||
'password' => 'Wachtwoord1',
|
||
]);
|
||
|
||
$regResponse->assertStatus(201);
|
||
|
||
// Person should be linked to parent (festival), not sub-event
|
||
$person = Person::where('email', 'festival@test.nl')->first();
|
||
$this->assertNotNull($person);
|
||
$this->assertEquals($festival->id, $person->event_id, 'Person should be linked to parent event');
|
||
$this->assertNotNull($person->user_id);
|
||
|
||
$user = User::where('email', 'festival@test.nl')->first();
|
||
|
||
// ── Step 2: Approve ──
|
||
Sanctum::actingAs($this->orgAdmin);
|
||
$this->postJson(
|
||
"/api/v1/organisations/{$this->organisation->id}/events/{$festival->id}/persons/{$person->id}/approve"
|
||
)->assertOk();
|
||
|
||
// ── Step 3: Portal access ──
|
||
Sanctum::actingAs($user);
|
||
|
||
// portal/me with parent event ID should work
|
||
$this->getJson("/api/v1/portal/me?event_id={$festival->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.email', 'festival@test.nl');
|
||
|
||
// portal/me with sub-event ID should also work (controller resolves to parent)
|
||
$this->getJson("/api/v1/portal/me?event_id={$subEvent->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.email', 'festival@test.nl');
|
||
}
|
||
|
||
/**
|
||
* Returning volunteer: existing user registers for new event.
|
||
*/
|
||
public function test_returning_volunteer_portal_access(): void
|
||
{
|
||
Mail::fake();
|
||
|
||
$existingUser = User::factory()->create([
|
||
'first_name' => 'Bestaand',
|
||
'last_name' => 'Lid',
|
||
'email' => 'bestaand@test.nl',
|
||
'password' => \Illuminate\Support\Facades\Hash::make('BestaandWw1'),
|
||
]);
|
||
|
||
// Register for event with existing account
|
||
$regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
|
||
'first_name' => 'Bestaand',
|
||
'last_name' => 'Lid',
|
||
'email' => 'bestaand@test.nl',
|
||
'password' => 'BestaandWw1',
|
||
]);
|
||
|
||
$regResponse->assertStatus(201);
|
||
|
||
$person = Person::where('email', 'bestaand@test.nl')->first();
|
||
$this->assertEquals($existingUser->id, $person->user_id);
|
||
|
||
// Approve
|
||
Sanctum::actingAs($this->orgAdmin);
|
||
$this->postJson(
|
||
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve"
|
||
)->assertOk();
|
||
|
||
// Portal access
|
||
Sanctum::actingAs($existingUser);
|
||
|
||
$this->getJson('/api/v1/auth/me')
|
||
->assertOk()
|
||
->assertJsonCount(1, 'data.portal_events');
|
||
|
||
$this->getJson("/api/v1/portal/me?event_id={$this->event->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.status', 'approved');
|
||
}
|
||
|
||
/**
|
||
* Organizer-created person (no user account) → identity match confirmed → portal/me.
|
||
*/
|
||
public function test_organizer_created_person_then_identity_linked(): void
|
||
{
|
||
// ── Step 1: Organizer creates a person manually ──
|
||
$user = User::factory()->create([
|
||
'first_name' => 'Handmatig',
|
||
'last_name' => 'Toegevoegd',
|
||
'email' => 'handmatig@test.nl',
|
||
]);
|
||
$this->organisation->users()->attach($user, ['role' => 'org_member']);
|
||
|
||
Sanctum::actingAs($this->orgAdmin);
|
||
|
||
$storeResponse = $this->postJson(
|
||
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons",
|
||
[
|
||
'crowd_type_id' => $this->volunteerCrowdType->id,
|
||
'first_name' => 'Handmatig',
|
||
'last_name' => 'Toegevoegd',
|
||
'email' => 'handmatig@test.nl',
|
||
'status' => 'approved',
|
||
]
|
||
);
|
||
|
||
$storeResponse->assertStatus(201);
|
||
|
||
$person = Person::where('email', 'handmatig@test.nl')->first();
|
||
$this->assertNotNull($person);
|
||
$this->assertNull($person->user_id, 'Organizer-created person should not have user_id');
|
||
|
||
// ── Step 2: Portal access should fail (no user_id link) ──
|
||
Sanctum::actingAs($user);
|
||
|
||
$this->getJson("/api/v1/portal/me?event_id={$this->event->id}")
|
||
->assertStatus(404);
|
||
|
||
// ── Step 3: Confirm identity match → links user_id ──
|
||
$match = $person->pendingIdentityMatch;
|
||
$this->assertNotNull($match, 'Identity match should have been auto-detected');
|
||
|
||
Sanctum::actingAs($this->orgAdmin);
|
||
|
||
$this->postJson(
|
||
"/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm"
|
||
)->assertOk();
|
||
|
||
$person->refresh();
|
||
$this->assertEquals($user->id, $person->user_id, 'user_id should be set after identity confirm');
|
||
|
||
// ── Step 4: Portal access should now work ──
|
||
Sanctum::actingAs($user);
|
||
|
||
$this->getJson("/api/v1/portal/me?event_id={$this->event->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.email', 'handmatig@test.nl');
|
||
}
|
||
|
||
/**
|
||
* Pending volunteer can see their status via portal (before approval).
|
||
*/
|
||
public function test_pending_volunteer_can_access_portal_me(): void
|
||
{
|
||
Mail::fake();
|
||
|
||
$this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
|
||
'first_name' => 'Wachtend',
|
||
'last_name' => 'Vrijwilliger',
|
||
'email' => 'wachtend@test.nl',
|
||
'password' => 'Wachtwoord1',
|
||
])->assertStatus(201);
|
||
|
||
$user = User::where('email', 'wachtend@test.nl')->first();
|
||
Sanctum::actingAs($user);
|
||
|
||
// Even without approval, portal/me should work (just shows pending status)
|
||
$this->getJson("/api/v1/portal/me?event_id={$this->event->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.status', 'pending');
|
||
}
|
||
}
|