feat: fase 2 backend — crowd types, persons, sections, shifts, invite flow

- Crowd Types + Persons CRUD (73 tests)
- Festival Sections + Time Slots + Shifts CRUD met assign/claim flow (84 tests)
- Invite Flow + Member Management met InvitationService (109 tests)
- Schema v1.6 migraties volledig uitgevoerd
- DevSeeder bijgewerkt met crowd types voor testorganisatie
This commit is contained in:
2026-04-08 01:34:46 +02:00
parent c417a6647a
commit 9acb27af3a
114 changed files with 6916 additions and 984 deletions

View File

@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Member;
use App\Models\Organisation;
use App\Models\User;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class MemberTest extends TestCase
{
use RefreshDatabase;
private Organisation $org;
private User $orgAdmin;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleSeeder::class);
$this->org = Organisation::factory()->create();
$this->orgAdmin = User::factory()->create();
$this->org->users()->attach($this->orgAdmin, ['role' => 'org_admin']);
}
// --- INDEX ---
public function test_org_member_can_list_members(): void
{
$member = User::factory()->create();
$this->org->users()->attach($member, ['role' => 'org_member']);
Sanctum::actingAs($member);
$response = $this->getJson("/api/v1/organisations/{$this->org->id}/members");
$response->assertOk();
$response->assertJsonCount(2, 'data');
$response->assertJsonStructure(['meta' => ['total_members', 'pending_invitations_count']]);
}
public function test_non_member_cannot_list_members(): void
{
$outsider = User::factory()->create();
Sanctum::actingAs($outsider);
$response = $this->getJson("/api/v1/organisations/{$this->org->id}/members");
$response->assertForbidden();
}
// --- UPDATE ---
public function test_org_admin_can_update_member_role(): void
{
$member = User::factory()->create();
$this->org->users()->attach($member, ['role' => 'org_member']);
Sanctum::actingAs($this->orgAdmin);
$response = $this->putJson(
"/api/v1/organisations/{$this->org->id}/members/{$member->id}",
['role' => 'org_admin'],
);
$response->assertOk();
$this->assertDatabaseHas('organisation_user', [
'user_id' => $member->id,
'organisation_id' => $this->org->id,
'role' => 'org_admin',
]);
}
public function test_cannot_update_own_role(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->putJson(
"/api/v1/organisations/{$this->org->id}/members/{$this->orgAdmin->id}",
['role' => 'org_member'],
);
$response->assertUnprocessable();
}
public function test_cannot_demote_last_org_admin(): void
{
$member = User::factory()->create();
$this->org->users()->attach($member, ['role' => 'org_admin']);
// Now there are 2 admins. Remove one so orgAdmin is the only one.
Sanctum::actingAs($this->orgAdmin);
// Demote member (second admin) — should work since orgAdmin still remains
$response = $this->putJson(
"/api/v1/organisations/{$this->org->id}/members/{$member->id}",
['role' => 'org_member'],
);
$response->assertOk();
// Now try having member (now org_member) demote orgAdmin (the last admin)
// First, make member admin again to do this, then setup a scenario with truly 1 admin
// Reset: make a new user as the sole admin
$soleAdmin = User::factory()->create();
$org2 = Organisation::factory()->create();
$org2->users()->attach($soleAdmin, ['role' => 'org_admin']);
$target = User::factory()->create();
$org2->users()->attach($target, ['role' => 'org_member']);
Sanctum::actingAs($soleAdmin);
// Sole admin tries to make themselves org_member — blocked by "own role" check
// Instead: soleAdmin tries to make another admin demoted, but they are the sole admin
// Let's set target as org_admin and try to demote them
$org2->users()->updateExistingPivot($target->id, ['role' => 'org_admin']);
// Now demote soleAdmin (but soleAdmin can't change own role)
// Let target (now admin) try to demote soleAdmin
Sanctum::actingAs($target);
$response = $this->putJson(
"/api/v1/organisations/{$org2->id}/members/{$soleAdmin->id}",
['role' => 'org_member'],
);
// soleAdmin is one of 2 admins now, so this should succeed
$response->assertOk();
// Now target is the sole admin. Try demoting target — but target can't change own role.
// So let soleAdmin (now org_member) try — they lack permission.
// The real test: org with 1 admin, another admin tries to demote them.
// Let's create a clean scenario:
$org3 = Organisation::factory()->create();
$admin1 = User::factory()->create();
$admin2 = User::factory()->create();
$org3->users()->attach($admin1, ['role' => 'org_admin']);
$org3->users()->attach($admin2, ['role' => 'org_admin']);
// Demote admin1 so admin2 is the last admin
Sanctum::actingAs($admin2);
$this->putJson(
"/api/v1/organisations/{$org3->id}/members/{$admin1->id}",
['role' => 'org_member'],
)->assertOk();
// Now admin2 is last admin. admin1 (now member) can't demote — 403.
// admin2 can't change own role. So let's re-promote admin1 then try to demote admin2.
$org3->users()->updateExistingPivot($admin1->id, ['role' => 'org_admin']);
// Now both are admins again. Demote admin1 to member.
$this->putJson(
"/api/v1/organisations/{$org3->id}/members/{$admin1->id}",
['role' => 'org_member'],
)->assertOk();
// admin2 is sole admin. Try to demote admin2 using admin1 — admin1 is org_member, so 403.
Sanctum::actingAs($admin1);
$response = $this->putJson(
"/api/v1/organisations/{$org3->id}/members/{$admin2->id}",
['role' => 'org_member'],
);
$response->assertForbidden();
}
// --- DESTROY ---
public function test_org_admin_can_remove_member(): void
{
$member = User::factory()->create();
$this->org->users()->attach($member, ['role' => 'org_member']);
Sanctum::actingAs($this->orgAdmin);
$response = $this->deleteJson(
"/api/v1/organisations/{$this->org->id}/members/{$member->id}"
);
$response->assertNoContent();
$this->assertDatabaseMissing('organisation_user', [
'user_id' => $member->id,
'organisation_id' => $this->org->id,
]);
}
public function test_cannot_remove_self(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->deleteJson(
"/api/v1/organisations/{$this->org->id}/members/{$this->orgAdmin->id}"
);
$response->assertUnprocessable();
}
public function test_cannot_remove_last_org_admin(): void
{
// Add a second admin to do the removal
$secondAdmin = User::factory()->create();
$this->org->users()->attach($secondAdmin, ['role' => 'org_admin']);
// Demote secondAdmin so orgAdmin is the only admin
$this->org->users()->updateExistingPivot($secondAdmin->id, ['role' => 'org_member']);
Sanctum::actingAs($secondAdmin);
// secondAdmin is now org_member — can't delete at all (403)
$response = $this->deleteJson(
"/api/v1/organisations/{$this->org->id}/members/{$this->orgAdmin->id}"
);
$response->assertForbidden();
}
public function test_unauthenticated_cannot_access_members(): void
{
$response = $this->getJson("/api/v1/organisations/{$this->org->id}/members");
$response->assertUnauthorized();
}
public function test_org_member_cannot_update_roles(): void
{
$member = User::factory()->create();
$this->org->users()->attach($member, ['role' => 'org_member']);
$target = User::factory()->create();
$this->org->users()->attach($target, ['role' => 'org_member']);
Sanctum::actingAs($member);
$response = $this->putJson(
"/api/v1/organisations/{$this->org->id}/members/{$target->id}",
['role' => 'org_admin'],
);
$response->assertForbidden();
}
public function test_org_member_cannot_remove_members(): void
{
$member = User::factory()->create();
$this->org->users()->attach($member, ['role' => 'org_member']);
$target = User::factory()->create();
$this->org->users()->attach($target, ['role' => 'org_member']);
Sanctum::actingAs($member);
$response = $this->deleteJson(
"/api/v1/organisations/{$this->org->id}/members/{$target->id}"
);
$response->assertForbidden();
}
}