Files
crewli/api/tests/Feature/Organisation/OrganisationDashboardStatsTest.php
bert.hausmans 671e0c9889 feat(organisation): add dashboard-stats endpoint
GET /organisations/{organisation}/dashboard-stats returns members,
events (with status breakdown + active count), persons, the first five
members sorted by join date, and the five most recent activity log
entries. Business logic lives in OrganisationDashboardService; access
follows OrganisationPolicy@view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:27:37 +02:00

161 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Organisation;
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 Laravel\Sanctum\Sanctum;
use Spatie\Activitylog\Models\Activity;
use Tests\TestCase;
class OrganisationDashboardStatsTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleSeeder::class);
}
public function test_unauthenticated_request_returns_401(): void
{
$org = Organisation::factory()->create();
$response = $this->getJson("/api/v1/organisations/{$org->id}/dashboard-stats");
$response->assertUnauthorized();
}
public function test_member_from_other_org_returns_403(): void
{
$user = User::factory()->create();
$otherOrg = Organisation::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson("/api/v1/organisations/{$otherOrg->id}/dashboard-stats");
$response->assertForbidden();
}
public function test_authenticated_member_can_fetch_dashboard_stats(): void
{
$user = User::factory()->create();
$org = Organisation::factory()->create();
$org->users()->attach($user, ['role' => 'org_member']);
Sanctum::actingAs($user);
$response = $this->getJson("/api/v1/organisations/{$org->id}/dashboard-stats");
$response->assertOk()
->assertJsonStructure([
'data' => [
'members_count',
'events_count',
'events_by_status',
'active_events_count',
'persons_count',
'top_members',
'recent_activity',
],
]);
}
public function test_stats_return_correct_counts(): void
{
$user = User::factory()->create();
$org = Organisation::factory()->create();
$org->users()->attach($user, ['role' => 'org_admin']);
// 2 extra members -> 3 total
$org->users()->attach(User::factory()->create(), ['role' => 'org_member']);
$org->users()->attach(User::factory()->create(), ['role' => 'org_member']);
// Events: 2 draft, 1 published, 1 buildup, 1 closed = 5 total, 2 active (published + buildup)
Event::factory()->count(2)->create(['organisation_id' => $org->id, 'status' => 'draft']);
$published = Event::factory()->create(['organisation_id' => $org->id, 'status' => 'published']);
Event::factory()->create(['organisation_id' => $org->id, 'status' => 'buildup']);
Event::factory()->create(['organisation_id' => $org->id, 'status' => 'closed']);
// 3 persons in the published event
Person::factory()->count(3)->create(['event_id' => $published->id]);
// Noise: another org with its own data — should be filtered out
$otherOrg = Organisation::factory()->create();
$otherOrg->users()->attach(User::factory()->create(), ['role' => 'org_admin']);
$otherEvent = Event::factory()->create(['organisation_id' => $otherOrg->id, 'status' => 'published']);
Person::factory()->create(['event_id' => $otherEvent->id]);
Sanctum::actingAs($user);
$response = $this->getJson("/api/v1/organisations/{$org->id}/dashboard-stats");
$response->assertOk();
$data = $response->json('data');
$this->assertSame(3, $data['members_count']);
$this->assertSame(5, $data['events_count']);
$this->assertSame(2, $data['active_events_count']);
$this->assertSame(3, $data['persons_count']);
$this->assertSame(2, $data['events_by_status']['draft']);
$this->assertSame(1, $data['events_by_status']['published']);
$this->assertSame(1, $data['events_by_status']['buildup']);
$this->assertSame(1, $data['events_by_status']['closed']);
$this->assertSame(0, $data['events_by_status']['registration_open']);
}
public function test_top_members_limited_to_five_and_ordered_by_joined_at(): void
{
$user = User::factory()->create();
$org = Organisation::factory()->create();
// Attach user first with the earliest join timestamp
$org->users()->attach($user, ['role' => 'org_admin']);
// Attach 6 more members (total 7)
for ($i = 0; $i < 6; $i++) {
$org->users()->attach(User::factory()->create(), ['role' => 'org_member']);
}
Sanctum::actingAs($user);
$response = $this->getJson("/api/v1/organisations/{$org->id}/dashboard-stats");
$response->assertOk();
$members = $response->json('data.top_members');
$this->assertCount(5, $members);
$this->assertSame($user->id, $members[0]['id']);
$this->assertSame('org_admin', $members[0]['role']);
}
public function test_recent_activity_limited_to_five_entries(): void
{
$user = User::factory()->create();
$org = Organisation::factory()->create();
$org->users()->attach($user, ['role' => 'org_admin']);
Activity::query()->delete();
// Trigger 7 activity log entries on the organisation
for ($i = 0; $i < 7; $i++) {
$org->update(['name' => "Changed {$i}"]);
}
Sanctum::actingAs($user);
$response = $this->getJson("/api/v1/organisations/{$org->id}/dashboard-stats");
$response->assertOk();
$this->assertCount(5, $response->json('data.recent_activity'));
}
}