fedi-feed-router/tests/Unit/Dashboard/Stats/ApprovalRateTest.php

149 lines
4.7 KiB
PHP

<?php
namespace Tests\Unit\Dashboard\Stats;
use App\Dashboard\Stats\ApprovalRate;
use App\Dashboard\Stats\DailySeriesStat;
use App\Enums\ApprovalStatusEnum;
use App\Models\RouteArticle;
use App\Support\DateRange;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;
class ApprovalRateTest extends TestCase
{
use RefreshDatabase;
private function range(): DateRange
{
return new DateRange(
Carbon::parse('2026-07-01 00:00:00'),
Carbon::parse('2026-07-31 23:59:59'),
);
}
/**
* @return array<string, float|null>
*/
private function values(): array
{
$result = (new ApprovalRate)->for($this->range());
return array_combine($result->labels, $result->series[0]->values);
}
private function decision(ApprovalStatusEnum $status, string $decidedAt): void
{
RouteArticle::factory()->create([
'approval_status' => $status,
'decided_at' => Carbon::parse($decidedAt),
]);
}
public function test_it_computes_the_approval_share_of_daily_decisions(): void
{
$this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 09:00:00');
$this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 10:00:00');
$this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 11:00:00');
$this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 12:00:00');
$this->assertSame(75.0, $this->values()['2026-07-10']);
}
public function test_it_excludes_pending_articles_from_the_denominator(): void
{
$this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 09:00:00');
RouteArticle::factory()->create([
'approval_status' => ApprovalStatusEnum::PENDING,
'decided_at' => null,
'created_at' => Carbon::parse('2026-07-10 09:00:00'),
]);
$this->assertSame(100.0, $this->values()['2026-07-10']);
}
public function test_it_reports_no_rate_for_a_day_with_no_decisions(): void
{
$this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 09:00:00');
$this->assertNull($this->values()['2026-07-11']);
}
public function test_it_reports_a_zero_rate_when_everything_was_rejected(): void
{
$this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 09:00:00');
$this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 10:00:00');
$this->assertSame(0.0, $this->values()['2026-07-10']);
}
public function test_it_buckets_by_decision_time_not_creation_time(): void
{
RouteArticle::factory()->create([
'approval_status' => ApprovalStatusEnum::APPROVED,
'created_at' => Carbon::parse('2026-07-05 09:00:00'),
'decided_at' => Carbon::parse('2026-07-20 09:00:00'),
]);
$values = $this->values();
$this->assertNull($values['2026-07-05']);
$this->assertSame(100.0, $values['2026-07-20']);
}
public function test_it_ignores_articles_decided_before_the_column_existed(): void
{
RouteArticle::factory()->create([
'approval_status' => ApprovalStatusEnum::APPROVED,
'created_at' => Carbon::parse('2026-07-10 09:00:00'),
'decided_at' => null,
]);
$this->assertNull($this->values()['2026-07-10']);
}
public function test_it_excludes_decisions_outside_the_range(): void
{
$this->decision(ApprovalStatusEnum::APPROVED, '2026-06-30 23:59:59');
$this->decision(ApprovalStatusEnum::APPROVED, '2026-08-01 00:00:00');
$this->assertSame(
[],
array_filter($this->values(), fn (?float $value): bool => $value !== null)
);
}
public function test_it_reports_a_meaningful_zero_as_data(): void
{
$this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 09:00:00');
$this->assertFalse((new ApprovalRate)->for($this->range())->hasNoData());
}
public function test_it_counts_every_decided_status_in_the_denominator(): void
{
$decided = array_filter(
ApprovalStatusEnum::cases(),
fn (ApprovalStatusEnum $status): bool => $status->isDecided(),
);
foreach ($decided as $status) {
$this->decision($status, '2026-07-10 09:00:00');
}
$this->assertSame(
round(100 / count($decided), 1),
$this->values()['2026-07-10'],
);
}
public function test_it_is_a_daily_series_stat(): void
{
$stat = new ApprovalRate;
$this->assertInstanceOf(DailySeriesStat::class, $stat);
$this->assertTrue($stat->for(DateRange::preset('all'))->isTooWide());
}
}